diff --git a/.clinerules b/.clinerules new file mode 100644 index 0000000..9832b18 --- /dev/null +++ b/.clinerules @@ -0,0 +1,23 @@ +# Cline Rules + +## ⚠️ PRIORITY ORDER — NON-NEGOTIABLE ⚠️ + +**Speed is never more important than following workspace procedures.** + +Before doing ANY work on ANY user request, you MUST complete these steps in order: + +1. **Read `AGENTS-README-FIRST.yaml`** in the repo root for the current API key and endpoints +2. **GET `/health`** to verify the MCP server is running +3. **POST `/mcp/sessionlog`** with your session entry — do NOT proceed until this succeeds +4. **GET `/mcp/sessionlog?limit=5`** to review recent session history for context +5. **GET `/mcp/todo`** to check current tasks +6. **THEN** begin working on the user's request + +On EVERY subsequent user message: +1. POST an updated session log entry BEFORE starting work +2. Complete the user's request +3. POST the final session log with results, actions taken, and files modified + +**If you skip any of these steps, STOP and go back and do them before continuing.** +Session logging is not optional, not deferred, and not secondary to the task. +Failure to maintain the session log is a compliance violation. \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fec0f3e..ed8c60b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,5 +1,7 @@ # GitHub Copilot Instructions for Remote Agent +For specific operational instructions (session bootstrap, session log turn workflow, and helper command sequence), follow `AGENTS-README-FIRST.yaml`. + ## Development Branch Strategy **IMPORTANT: Always work in the `develop` branch for all development tasks.** @@ -22,6 +24,17 @@ ## Code Quality Standards +### UI Buttons — CQRS Required + +**Every button added to the Desktop UI must be backed by a CQRS request/handler pair with complete tests.** + +- Create `*Request` in `src/RemoteAgent.Desktop/Requests/` and `*Handler` in `src/RemoteAgent.Desktop/Handlers/` +- Register the handler as a transient in `App.axaml.cs` `ConfigureServices` +- The ViewModel command **must dispatch via `IRequestDispatcher`** — no handler logic in the ViewModel +- Add `*HandlerTests.cs` in `tests/RemoteAgent.Desktop.UiTests/Handlers/` covering success, failure, and edge cases +- Add a `Null*` stub for any new infrastructure interface to `SharedHandlerTestStubs.cs` +- See `docs/REPOSITORY_RULES.md` for full details and the `CopyStatusLogHandler` as a reference implementation + ### Warnings as Errors - All warnings are treated as errors (see `Directory.Build.props`) @@ -101,3 +114,5 @@ Before completing any task: ✅ **Sign commits** ✅ **Run tests before completing tasks** ✅ **Use bash only in workflows (no Python)** +✅ **Every UI button → CQRS request/handler + complete tests** + diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 05aa33e..b84a53a 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -197,6 +197,79 @@ jobs: dotnet restore RemoteAgent.Desktop.csproj --configfile ../../NuGet.Config -v minimal -p:TargetFramework=net9.0 -p:TargetFrameworks=net9.0 dotnet build RemoteAgent.Desktop.csproj -c Release --no-restore -v minimal -p:TargetFramework=net9.0 -p:TargetFrameworks=net9.0 + build-msix: + name: Build MSIX (Windows) + runs-on: windows-latest + needs: [detect, build] + if: needs.detect.outputs.code == 'true' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Setup .NET 9 (desktop) + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Build MSIX package + shell: pwsh + env: + VERSION: ${{ needs.build.outputs.semver }} + run: | + .\scripts\package-msix.ps1 -Version $env:VERSION -Force + + - name: Upload MSIX artifact + uses: actions/upload-artifact@v4 + with: + name: msix-${{ needs.build.outputs.major_minor_patch }} + path: | + artifacts/*.msix + artifacts/*.cer + if-no-files-found: error + retention-days: 30 + + build-deb: + name: Build .deb (Linux) + runs-on: ubuntu-latest + needs: [detect, build] + if: needs.detect.outputs.code == 'true' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Setup .NET 9 (desktop) + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Build .deb packages + env: + VERSION: ${{ needs.build.outputs.semver }} + run: | + chmod +x scripts/package-deb.sh + ./scripts/package-deb.sh --version "$VERSION" + + - name: Upload .deb artifacts + uses: actions/upload-artifact@v4 + with: + name: deb-${{ needs.build.outputs.major_minor_patch }} + path: artifacts/*.deb + if-no-files-found: error + retention-days: 30 + android: name: Build Android APK runs-on: ubuntu-latest @@ -273,7 +346,7 @@ jobs: release-beta: name: Create beta release runs-on: ubuntu-latest - needs: [build, desktop-build, android, docker] + needs: [build, desktop-build, android, docker, build-msix, build-deb] permissions: contents: write steps: @@ -313,6 +386,18 @@ jobs: with: name: android-apk + - name: Download MSIX artifacts + uses: actions/download-artifact@v4 + with: + name: msix-${{ needs.build.outputs.major_minor_patch }} + path: release-assets/msix + + - name: Download .deb artifacts + uses: actions/download-artifact@v4 + with: + name: deb-${{ needs.build.outputs.major_minor_patch }} + path: release-assets/deb + - name: Create beta release uses: softprops/action-gh-release@v2 with: @@ -320,7 +405,11 @@ jobs: name: Beta v${{ steps.version.outputs.version }} (build ${{ github.run_number }}) prerelease: true body_path: release-notes.md - files: com.companyname.remoteagent.app-Signed.apk + files: | + com.companyname.remoteagent.app-Signed.apk + release-assets/msix/*.msix + release-assets/msix/*.cer + release-assets/deb/*.deb docker: name: Build and push Docker image diff --git a/.github/workflows/build-msix.yml b/.github/workflows/build-msix.yml deleted file mode 100644 index d74d129..0000000 --- a/.github/workflows/build-msix.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: Build MSIX - -# Builds the combined Remote Agent MSIX package (service + desktop) on Windows. -# Triggers on pushes/PRs that touch the relevant source and on manual dispatch. - -on: - push: - branches: [main, develop] - paths: - - 'src/RemoteAgent.Service/**' - - 'src/RemoteAgent.Desktop/**' - - 'src/RemoteAgent.Plugins.Ollama/**' - - 'src/RemoteAgent.Proto/**' - - 'src/RemoteAgent.App.Logic/**' - - 'scripts/package-msix.ps1' - - 'scripts/install-remote-agent.ps1' - - '.github/workflows/build-msix.yml' - pull_request: - branches: [main, develop] - paths: - - 'src/RemoteAgent.Service/**' - - 'src/RemoteAgent.Desktop/**' - - 'src/RemoteAgent.Plugins.Ollama/**' - - 'src/RemoteAgent.Proto/**' - - 'src/RemoteAgent.App.Logic/**' - - 'scripts/package-msix.ps1' - - 'scripts/install-remote-agent.ps1' - - '.github/workflows/build-msix.yml' - workflow_dispatch: - inputs: - configuration: - description: Build configuration - required: false - default: Release - type: choice - options: [Release, Debug] - self_contained: - description: Publish self-contained (bundles .NET runtime) - required: false - default: false - type: boolean - sign: - description: Sign with dev certificate - required: false - default: false - type: boolean - -jobs: - build-msix: - name: Build MSIX (Windows) - runs-on: windows-latest - - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # full history for GitVersion - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Setup .NET 9 (desktop) - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v1 - with: - versionSpec: '6.x' - - - name: Get version - id: version - uses: gittools/actions/gitversion/execute@v1 - with: - useConfigFile: true - - - name: Build MSIX package - shell: pwsh - env: - CONFIGURATION: ${{ github.event.inputs.configuration || 'Release' }} - SELF_CONTAINED: ${{ github.event.inputs.self_contained || 'false' }} - SIGN: ${{ github.event.inputs.sign || 'false' }} - VERSION: ${{ steps.version.outputs.MajorMinorPatch }} - run: | - $params = @{ - Configuration = $env:CONFIGURATION - Version = $env:VERSION - OutDir = "artifacts" - } - if ($env:SELF_CONTAINED -eq 'true') { $params['SelfContained'] = $true } - if ($env:SIGN -eq 'true') { $params['DevCert'] = $true } - .\scripts\package-msix.ps1 @params - - - name: Upload MSIX artifact - uses: actions/upload-artifact@v4 - with: - name: remote-agent-msix-${{ steps.version.outputs.MajorMinorPatch }} - path: | - artifacts/*.msix - artifacts/*.cer - if-no-files-found: error - retention-days: 30 - - - name: Upload install script - uses: actions/upload-artifact@v4 - with: - name: remote-agent-install-scripts - path: | - scripts/install-remote-agent.ps1 - scripts/package-msix.ps1 - retention-days: 30 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bd1dfdd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "scripts/MsixTools"] + path = scripts/MsixTools + url = https://github.com/sharpninja/MsixTools.git diff --git a/.vscode/settings.json b/.vscode/settings.json index 70ca5cc..86ecaf1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "path": "pwsh.exe", "args": ["-NoProfile"] } - } + }, + "fusion-360-helper.enabled": false } diff --git a/AGENTS.md b/AGENTS.md index fa68646..7d3dd4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,27 @@ # Repository Guidelines +## ⚠️ PRIORITY ORDER — NON-NEGOTIABLE ⚠️ + +**Speed is never more important than following workspace procedures.** + +Before doing ANY work on ANY user request, you MUST complete these steps in order: + +1. **Read `AGENTS-README-FIRST.yaml`** in the repo root for the current API key and endpoints +2. **GET `/health`** to verify the MCP server is running +3. **POST `/mcp/sessionlog`** with your session turn — do NOT proceed until this succeeds +4. **GET `/mcp/sessionlog?limit=5`** to review recent session history for context +5. **GET `/mcp/todo`** to check current tasks +6. **THEN** begin working on the user's request + +On EVERY subsequent user message: +1. Post a new session log turn (`Add-McpSessionTurn`) before starting work. +2. Complete the user's request. +3. Update the turn with results (`Response`) and actions (`Add-McpAction`) when done. + +**If you skip any of these steps, STOP and go back and do them before continuing.** +Session logging is not optional, not deferred, and not secondary to the task. +Failure to maintain the session log is a compliance violation. + ## Project Structure & Module Organization - `src/RemoteAgent.App`: .NET MAUI Android client UI and platform services. - `src/RemoteAgent.Service`: ASP.NET Core gRPC service that runs and streams agent sessions. @@ -43,3 +65,4 @@ Use default/minimal verbosity. Do not pass `-q` to `dotnet build` or `dotnet res - Use concise imperative commit messages, optionally with issue references (example: `Fix Android CI job (#8)`). - Sign commits (verified signature required by branch protection). - PRs should include: purpose, summary of changes, test evidence (`dotnet test`), and docs updates when behavior/config changes. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9207169..4b8544d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,34 @@ All warnings are treated as errors. See [REPOSITORY_RULES.md](docs/REPOSITORY_RU - Ensure all tests pass before submitting a PR. - Integration tests should go in `RemoteAgent.Service.IntegrationTests`. +### Requirements Traceability + +All test classes and methods should be annotated with the functional (FR) and technical (TR) requirements they cover: + +```csharp +/// Tests for authentication. FR-13.5; TR-18.1, TR-18.2. +[Trait("Category", "Requirements")] +[Trait("Requirement", "FR-13.5")] +[Trait("Requirement", "TR-18.1")] +[Trait("Requirement", "TR-18.2")] +public class AuthUserServiceTests +{ + [Fact] + public void UpsertListDelete_ShouldPersistUser() + { + // Test implementation + } +} +``` + +After adding or updating requirement annotations, regenerate the traceability matrix: + +```bash +./scripts/generate-requirements-matrix.sh +``` + +This updates `docs/requirements-test-coverage.md`, which maps all requirements to their test coverage. + ## Building and Testing ### Prerequisites diff --git a/GitVersion.yml b/GitVersion.yml index 296ef4d..24e2672 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -2,4 +2,26 @@ # See https://gitversion.net/docs/reference/configuration workflow: GitHubFlow/v1 # Start at 1.0.0 when no version tag exists yet -next-version: 1.0.0 +next-version: 0.1.23 + + + + + + + + + + + + + + + + + + + + + + diff --git a/RECOVERED_INPUT.md b/RECOVERED_INPUT.md deleted file mode 100644 index 8f13d4b..0000000 --- a/RECOVERED_INPUT.md +++ /dev/null @@ -1,29 +0,0 @@ -# Recovered input from crashed session - -Recovered from Cursor chat **Project Initializer** -(`~/.cursor/chats/.../7ab2a9f8-0aea-49dc-8018-b06edc0195f4/store.db`). - -This was the **last user request** in that session (likely the one that was in progress or queued when the session crashed): - ---- - -**User request:** - -The server should support pushing chat messages with a priority (normal, high, notify). When a chat message with `notify` priority arrives, a system notification should be registered, and if the user taps it, the chat should make the message visible to the user. The user can also swipe a message left or right to archive it. - ---- - -## Other user messages from same chat (for context) - -- Create remote-agent folder, git init with VS + Cursor gitignore, blank .NET 10 solution -- Android app (MAUI) talking to Linux service; service spawns Cursor agent, gRPC bidirectional streaming, chat UI, real-time output, logging -- Docker for service; pipeline → GitHub Pages (F-Droid-style), APK + container to GHCR -- Markdown parser for Cursor output in chat -- Thorough unit and integration tests -- Font Awesome icons; light and dark mode -- Material Design UI norms -- **→ Priority + notify + swipe to archive** (above) - ---- - -*Recovered by reading Cursor chat SQLite store. Cursor does not persist an unsent “input queue”; only messages that were already sent are stored. If you had typed something and not yet sent it, it would not be recoverable.* diff --git a/SESSION_HANDOFF_2026-02-18T12-26-48-0600.md b/SESSION_HANDOFF_2026-02-18T12-26-48-0600.md deleted file mode 100644 index 37accad..0000000 --- a/SESSION_HANDOFF_2026-02-18T12-26-48-0600.md +++ /dev/null @@ -1,50 +0,0 @@ -# Session Handoff - -## Timestamp -Generated: 2026-02-18T12:26:48-06:00 - -## Completed In This Session -- **Exhaustive CQRS test matrix:** Expanded Phase 3 from ~100 estimated tests to ~237 individually specified tests. Every CQRS handler now has an explicit list of all known happy paths and all known failure paths (Section 6 of the implementation plan, plus per-test checkboxes in the TODO). -- **Handler entry/exit logging:** Added requirement and design for Debug-level log messages on every CQRS command/query entry and exit. Logging is a cross-cutting concern in the dispatcher — handlers do not log their own entry/exit. Log format includes request type, parameters, result (or exception), and CorrelationId. Sensitive-data redaction policy for `ToString()` overrides on request records with `ApiKey`/`Password`/`Token`/`Secret` fields (Section 2.6). -- **CorrelationId tracing:** Added required `Guid CorrelationId` property to `IRequest`. Every request record carries it as its first positional parameter. VMs generate `Guid.NewGuid()` at the UI interaction boundary; handlers propagate `request.CorrelationId` to any child dispatches. Dispatcher validates non-empty and includes `[{CorrelationId}]` in all log messages for end-to-end tracing of individual UI interactions (Section 2.8). Six multi-step handlers identified that must propagate CorrelationId. -- **Updated implementation plan:** `docs/implementation-plan-mvvm-cqrs-refactor.md` — Sections 2.1, 2.2, 2.5, 2.6 updated; new Sections 2.8 (CorrelationId design) and 6.5.31 (redaction tests), 6.5.32 (CorrelationId contract + propagation tests) added; test count tables, file checklists, and phase tasks updated throughout. -- **Updated TODO checklist:** `docs/implementation-plan-todo.md` — expanded to ~180 checkboxes with per-test-case granularity, CorrelationId items in Phase 0/1/2, and new Phase 3 sub-sections (3e redaction, 3f CorrelationId propagation, 3g mocks, 3h UI tests). - -## Current User Directives (Highest Priority) -1. No code-behind logic in desktop or mobile UI. -2. Strict MVVM adherence. -3. Strict CQRS adherence for all UI commands/events. -4. All CQRS commands and queries must have exhaustive unit tests with known happy paths and known failure paths. -5. All CQRS commands and queries must write Debug-level log messages upon entering and leaving, including parameters and results. -6. Every command/request must have a required `CorrelationId` parameter tracked through to subsequent commands/queries for tracing individual UI interactions. -7. Complete migration across both apps and ensure all unit, integration, and UI tests pass. - -## Current State -- The repo is in a broad in-progress refactor state with many modified files across desktop/mobile/service/docs. -- UI still contains existing code-behind paths that must be removed per directive. -- CQRS migration is fully designed at the plan level; implementation has not started. -- Implementation plan and TODO list are comprehensive and ready for execution; Phase 0 (shared CQRS foundation) is the next starting point. - -## Key Architecture Decisions -- **`IRequest`** requires `Guid CorrelationId { get; }` — not a bare marker interface. -- **`ServiceProviderRequestDispatcher`** handles all cross-cutting concerns: CorrelationId validation (`Guid.Empty` → `ArgumentException`), Debug entry/exit logging with `[{CorrelationId}]`, and reflection-based handler resolution. -- **Logging is dispatcher-only** — handlers do not log their own entry/exit. -- **CorrelationId generation** happens at the VM command boundary (`Guid.NewGuid()`); handlers propagate `request.CorrelationId` to child dispatches. -- **Sensitive-data redaction** — request records with `ApiKey`/`Password`/`Token`/`Secret` override `ToString()` and must still include `CorrelationId`. -- **~237 tests** specified: 9 infrastructure, 9 dialog VM, 133 desktop handlers, 65 mobile handlers, 5 redaction, 8 CorrelationId contract/propagation, 8 UI with mock dispatcher. -- **CommunityToolkit.Mvvm** adoption deferred to a follow-up refactor. - -## Next Steps -1. **Phase 0:** Add CQRS interfaces (`IRequest` with `CorrelationId`, `IRequestHandler`, `IRequestDispatcher`), `Unit`, `CommandResult` in `App.Logic/Cqrs/`; implement `ServiceProviderRequestDispatcher` with logging + CorrelationId validation; wire one Desktop flow (`SetManagementSectionRequest`); add attached behaviors; add dispatcher + handler unit tests (including logging and CorrelationId tests). -2. **Phase 1:** Remove desktop code-behind; add `ConnectionSettingsDialogViewModel`; `OpenNewSessionRequest`/handler with CorrelationId propagation to `CreateDesktopSessionRequest`; decompose `ServerWorkspaceViewModel` into 8 sub-VMs; implement all 24 desktop CQRS handlers. -3. **Phase 2:** Mobile platform abstractions (7 interfaces + implementations); replace `MainPage` delegates with request/handlers; `AppShellViewModel` + `McpRegistryPageViewModel`; implement all 17 mobile CQRS handlers. -4. **Phase 3:** ~237 unit tests per the exhaustive test matrix; CorrelationId propagation tests for 6 multi-step handlers; redaction tests; UI tests with mock dispatcher. -5. **Phase 4:** Full build/test; logging + redaction audit; code-behind audit; update requirements matrix and mark TR-18.x complete. - -*See:* `docs/implementation-plan-mvvm-cqrs-refactor.md`, `docs/implementation-plan-todo.md`. - -## Notes -- Follow AGENTS rule: avoid introducing code-behind logic; enforce MVVM + CQRS only. -- Preserve existing unrelated worktree changes; do not revert user changes. -- Every request record: `CorrelationId` is always the first positional parameter. -- Phase dependency order: 0 → 1 → 3a → 2 → 3b → 4 (sequential, not parallel). diff --git a/SESSION_HANDOFF_2026-02-19T00-04-44-0600.md b/SESSION_HANDOFF_2026-02-19T00-04-44-0600.md deleted file mode 100644 index 6f1f2df..0000000 --- a/SESSION_HANDOFF_2026-02-19T00-04-44-0600.md +++ /dev/null @@ -1,141 +0,0 @@ -# Session Handoff — 2026-02-19T00:04:44-0600 - -## Status: MVVM + CQRS Refactor — COMPLETE ✅ - -All phases of the MVVM + CQRS refactor are finished. All 67 FRs and 112 TRs are marked Done. -**240 tests passing** (89 `App.Tests` + 151 `Desktop.UiTests`), 0 failures. - ---- - -## Completed This Session - -### Commits (unpushed, on `develop`) - -| SHA | Message | -|-----|---------| -| `f7839ab` | test: Phase 3d — mobile handler unit tests (8 handlers, 22 tests) | -| `95c9313` | docs: mark FR-12.12 Done, update test count to 240, mark all todos complete | - -### Phase 3d — Mobile handler unit tests (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) -- 22 new tests covering all 8 mobile CQRS handlers: - `ConnectMobileSession`, `DisconnectMobileSession`, `CreateMobileSession`, - `TerminateMobileSession`, `SendMobileMessage`, `SendMobileAttachment`, - `ArchiveMobileMessage`, `UsePromptTemplate` -- `App.Tests` grew from 67 → 89 (+22) - -### Bug fixes during test authoring -| File | Fix | -|------|-----| -| `ConnectMobileSessionHandler.cs` | CS8601: `workspace.Host = host ?? ""` (null-coalesce) | -| `ConnectMobileSessionHandler.cs` | Removed `NotifyConnectionStateChanged()` from catch block — it was overwriting the "Failed:" status via `OnGatewayConnectionStateChanged()` | -| `McpRegistryPageViewModelTests.cs` | CS0104 ambiguity: added `using LogicRequests = RemoteAgent.App.Logic.Requests;` to distinguish `DeleteMcpServerRequest` from `RemoteAgent.Proto.DeleteMcpServerRequest` | -| `MobileHandlerTests.cs` | xUnit1031: made `Disconnect` test `async Task`, awaited handler result | - -### Docs -- `requirements-completion-summary.md`: FR-12.12 `Pending` → `Done` -- `requirements-completion-summary.md`: TR-18.4 test count `218` → `240` -- `implementation-plan-todo.md`: all 318 remaining `[ ]` items marked `[x]` - ---- - -## Architecture Overview - -### Project Structure -``` -src/ - RemoteAgent.Proto/ — shared protobuf contracts + generated gRPC C# - RemoteAgent.App.Logic/ — shared CQRS foundation, interfaces, VMs, handlers (net10.0) - RemoteAgent.App/ — MAUI Android client (net10.0-android) - RemoteAgent.Desktop/ — Avalonia desktop management app (net9.0) - RemoteAgent.Service/ — ASP.NET Core gRPC service (net10.0) -tests/ - RemoteAgent.App.Tests/ — 89 tests (net10.0) - RemoteAgent.Desktop.UiTests/— 151 tests (net9.0) - RemoteAgent.Service.IntegrationTests/ — isolated integration tests -``` - -### CQRS Pattern -- `IRequest` — all requests carry `Guid CorrelationId` as first property -- `IRequestHandler` — stateless handlers; no entry/exit logging -- `IRequestDispatcher` / `ServiceProviderRequestDispatcher` — sole cross-cutting concern; Debug-level entry/exit logging with `[{CorrelationId}]`; throws `ArgumentException` on `Guid.Empty` -- VMs generate `Guid.NewGuid()` at the UI command boundary -- All 32 Desktop handlers + 8 mobile (App) handlers + 3 App.Logic handlers - -### Sub-VM Decomposition (`ServerWorkspaceViewModel`) -`ServerWorkspaceViewModel` implements `IServerConnectionContext` and owns 6 sub-VMs: -- `Security` → `SecurityViewModel` — peers, bans, open sessions -- `AuthUsers` → `AuthUsersViewModel` — auth users, permission roles -- `Plugins` → `PluginsViewModel` — plugin assemblies + runner IDs -- `McpRegistry` → `McpRegistryDesktopViewModel` — MCP servers + agent mappings -- `PromptTemplates` → `PromptTemplatesViewModel` — prompt templates + seed context -- `StructuredLogs` → `StructuredLogsViewModel` — log monitoring + filtering - -Sub-VMs read connection info (Host/Port/ApiKey/AgentId) from `IServerConnectionContext` at command execution time — not at construction — to avoid stale data. - -### Management App Log (FR-12.12) -- `AppLoggerProvider` + `AppLogEntry` + `IAppLogStore` / `InMemoryAppLogStore` — captures all desktop `ILogger` output -- `AppLogViewModel` — observable collection with filter -- `ClearAppLogHandler` / `SaveAppLogHandler` — save to txt/json/csv via `IFileSaveDialogService` -- Registered via `ILoggerFactory.AddProvider(...)` in `App.axaml.cs` - -### Mobile CQRS -- `MainPageViewModel` — 12-param constructor; `IRequestDispatcher` is 12th param -- `ISessionCommandBus` — implemented by `MainPageViewModel`; DI forwarding factory -- All 8 mobile handlers in `src/RemoteAgent.App/Handlers/` -- 3 App.Logic handlers: `LoadMcpServersHandler`, `SaveMcpServerHandler`, `DeleteMcpServerHandler` - ---- - -## Test Infrastructure - -### Desktop (`tests/RemoteAgent.Desktop.UiTests/`) -- `Handlers/SharedHandlerTestStubs.cs` — shared stubs: `StubCapacityClient`, `FakeAgentSession`, `StubLogStore`, `StubSessionFactory`, `StubAppLogStore`, `StubStructuredLogClient`, `NullDispatcher`, `NullFileSaveDialogService`, `SharedWorkspaceFactory` -- 30 handler test files (one per handler) + `AppLogTests.cs` + `MainWindowUiTests.cs` - -### Mobile (`tests/RemoteAgent.App.Tests/`) -- `MobileHandlerTests.cs` — stubs: `StubGateway`, `StubSessionStore`, `StubApiClient`, `StubConnectionModeSelector`, `StubAgentSelector`, `StubAttachmentPicker`, `StubPromptTemplateSelector`, `NullAppPreferences`, `NullRequestDispatcher`, `CreateWorkspace()` factory -- `MauiStubs.cs` — additional MAUI-specific stub implementations -- `McpRegistryPageViewModelTests.cs` — uses `LogicRequests` alias for `DeleteMcpServerRequest` disambiguation -- `Cqrs/` — dispatcher, CorrelationId, and infrastructure tests - ---- - -## Known Constraints -- `RemoteAgent.App` (MAUI) cannot be built on Linux (no Android SDK); `App.Logic` and `App.Tests` build fine -- `dotnet test` cannot take multiple `.csproj` arguments; run each project separately -- `build-desktop-dotnet9.sh` may fail due to .NET version mismatch; use `dotnet build` directly -- GPG signing unavailable in this environment; use `git commit --no-gpg-sign` -- Integration tests excluded from normal CI; run via `./scripts/test-integration.sh` - ---- - -## Suggested Next Work - -The MVVM + CQRS refactor is architecturally complete. Possible next areas: - -1. **Push `develop` to remote** — 2 commits (`f7839ab`, `95c9313`) are unpushed -2. **Phase 3c gap** — the plan specified 133 Desktop handler tests; current count is 151 but some test scenarios (CorrelationId propagation, `ToString()` redaction) may be sparse -3. **Service integration tests** — `tests/RemoteAgent.Service.IntegrationTests/` may have coverage gaps -4. **Documentation site** — `docs/` uses DocFX; could regenerate `_site/` with updated content -5. **CI pipeline** — review `.github/workflows/build-deploy.yml` for any improvements -6. **Feature work** — review `docs/functional-requirements.md` for any unimplemented FRs beyond the refactor scope - ---- - -## Build Commands -```bash -# App.Logic + tests (net10.0, works on Linux) -dotnet build src/RemoteAgent.App.Logic/RemoteAgent.App.Logic.csproj -c Release -dotnet test tests/RemoteAgent.App.Tests/ -c Release - -# Desktop (net9.0) -dotnet build src/RemoteAgent.Desktop/RemoteAgent.Desktop.csproj -c Release -dotnet test tests/RemoteAgent.Desktop.UiTests/ -c Release - -# Service (net10.0) -dotnet build src/RemoteAgent.Service/RemoteAgent.Service.csproj -c Release -dotnet run --project src/RemoteAgent.Service - -# Integration tests (isolated) -./scripts/test-integration.sh Release -``` diff --git a/SESSION_HANDOFF_2026-02-20T05-22-27-0000.md b/SESSION_HANDOFF_2026-02-20T05-22-27-0000.md new file mode 100644 index 0000000..2c349f3 --- /dev/null +++ b/SESSION_HANDOFF_2026-02-20T05-22-27-0000.md @@ -0,0 +1,279 @@ +# Session Handoff — 2026-02-20T05:22 UTC + +## Project Overview + +**Remote Agent** — A system consisting of: +- **.NET MAUI Android app** (`net10.0-android`) — client that connects to a gRPC service, manages chat sessions with AI agents +- **ASP.NET Core gRPC service** (`net10.0`) — runs agent sessions, handles pairing/auth +- **Avalonia desktop management app** (`net9.0`) — admin tool for configuring the service (pairing users, API keys, server profiles, session management) + +--- + +## Repository Layout + +``` +src/ + RemoteAgent.App/ — .NET MAUI Android client + RemoteAgent.App.Logic/ — Shared logic, CQRS interfaces, platform abstractions (net9.0 + net10.0) + RemoteAgent.Desktop/ — Avalonia desktop management UI (net9.0) + RemoteAgent.Proto/ — Protobuf contracts + generated gRPC C# (net9.0 + net10.0) + RemoteAgent.Service/ — ASP.NET Core gRPC + web service (net10.0) +tests/ + RemoteAgent.App.Tests/ — Mobile app unit tests (net10.0, 117 tests) + RemoteAgent.Service.Tests/ — Service unit tests (net10.0, 27 tests) + RemoteAgent.Desktop.UiTests/— Avalonia desktop handler tests (net9.0, 173 tests) + RemoteAgent.Service.IntegrationTests/ — Isolated integration tests (run manually) +scripts/ + build-dotnet10.sh — Builds + tests MAUI app + service stack + build-desktop-dotnet9.sh — Builds + tests Avalonia desktop stack + test-integration.sh — Runs service integration tests (not part of CI) + generate-proto.sh — Regenerates gRPC C# from .proto (WSL/Linux only) +``` + +**Multi-targeting:** `RemoteAgent.App.Logic` and `RemoteAgent.Proto` target `net9.0;net10.0`. Desktop uses net9.0 TFM, service/mobile use net10.0. + +--- + +## Build Commands + +```bash +# MAUI + Service stack (.NET 10) — includes App.Tests + Service.Tests +bash ./scripts/build-dotnet10.sh Release + +# Avalonia desktop stack (.NET 9) — includes Desktop.UiTests +bash ./scripts/build-desktop-dotnet9.sh Release + +# Build Android APK (Debug only — Release hits XAGNM7009 ARM64 issue): +dotnet publish src/RemoteAgent.App/RemoteAgent.App.csproj \ + -c Debug -f net10.0-android \ + -p:AndroidSdkDirectory=/home/sharpninja/Android/Sdk \ + -p:EmbedAssembliesIntoApk=true + +# Install + launch on emulator: +WIN_ADB="/mnt/c/Users/kingd/platform-tools-windows/platform-tools/adb.exe" +APK=$(find src/RemoteAgent.App/bin/Debug/net10.0-android -name '*-Signed.apk' | head -1) +$WIN_ADB -s emulator-5556 shell am force-stop com.companyname.remoteagent.app +$WIN_ADB -s emulator-5556 install -r "$(wslpath -w "$APK")" +$WIN_ADB -s emulator-5556 shell am start -n com.companyname.remoteagent.app/crc6411305d2bb8acc544.MainActivity + +# Screenshot: +$WIN_ADB -s emulator-5556 exec-out screencap -p > /tmp/screenshot.png + +# Desktop build (use dotnet build directly — build script hits NETSDK1045): +dotnet build src/RemoteAgent.Desktop/RemoteAgent.Desktop.csproj -c Release +dotnet test tests/RemoteAgent.Desktop.UiTests/RemoteAgent.Desktop.UiTests.csproj -c Release +``` + +--- + +## Current Test Counts (all passing as of this handoff) + +- `RemoteAgent.App.Tests` — **117 tests** (includes 7 new server-profile handler tests) +- `RemoteAgent.Service.Tests` — **27 tests** +- `RemoteAgent.Desktop.UiTests` — **173 tests** +- **Total: 317 tests, all green** + +--- + +## Architecture: CQRS Pattern (Mobile) + +All mobile UI actions dispatch through `IRequestDispatcher`: + +``` +ViewModel command → new XxxRequest(Guid, workspace) → IRequestDispatcher.SendAsync() + → DI resolves IRequestHandler → handler.HandleAsync() +``` + +**Request files:** `src/RemoteAgent.App/Requests/` +**Handler files:** `src/RemoteAgent.App/Handlers/` +**Registration:** `src/RemoteAgent.App/MauiProgram.cs` (transient handlers) +**Tests:** `tests/RemoteAgent.App.Tests/` (linked source files from App project via csproj ``) + +The test project can't reference `RemoteAgent.App` directly (MAUI workload). Instead, it links individual `.cs` files and uses `MauiStubs.cs` for minimal `Microsoft.Maui.Controls.Command` stub. + +### Adding a new CQRS command: +1. Create `XxxRequest.cs` in `Requests/` (implements `IRequest`) +2. Create `XxxHandler.cs` in `Handlers/` (implements `IRequestHandler`) +3. Register in `MauiProgram.cs`: `builder.Services.AddTransient, XxxHandler>()` +4. Add `` links in `RemoteAgent.App.Tests.csproj` for both files +5. Create `XxxHandlerTests.cs` in tests +6. Wire ViewModel: `new Command(async () => await RunAsync(new XxxRequest(Guid.NewGuid(), this)))` + +--- + +## Architecture: Pairing Flow + +1. Admin sets pairing credentials via Desktop → `SetPairingUsers` gRPC RPC → service saves hashed creds + generates API key → writes to `appsettings.json` +2. Mobile user enters host + port → taps **Login** → WebView opens `http://{host}:1{port}/pair` +3. User logs in → service redirects to `/pair/key` → shows API key + QR + deep link button +4. `PairLoginPage` extracts deep link via JS → parses `remoteagent://pair?key=...&host=...&port=...` +5. API key stored → **Connect** button enabled → gRPC connection established + +**Port convention:** +- gRPC: 5243 (Linux/Docker, default), 5244 (Windows) +- Web/browser: 15243 / 15244 (formula: `"1" + gRPCport`) +- Deep link contains gRPC port, not web port + +--- + +## Feature: Server Profiles + +**Fully implemented** on both mobile and desktop. + +### Mobile (`src/RemoteAgent.App/`) +- `ServerProfile` model + `IServerProfileStore` interface in `App.Logic/ServerProfile.cs` +- `LocalServerProfileStore` (LiteDB) in `Services/LocalServerProfileStore.cs` +- `SettingsPage.xaml` — lists saved servers, edit display name / per-request context / default session context +- **CQRS handlers:** `SaveServerProfileHandler`, `DeleteServerProfileHandler`, `ClearServerApiKeyHandler` +- `SettingsPageViewModel` dispatches all commands via `IRequestDispatcher` +- `ConnectMobileSessionHandler` auto-saves profile on successful connect +- API key status shown with "Clear API Key" button + +### Desktop (`src/RemoteAgent.Desktop/`) +- `ServerRegistration` model extended with `PerRequestContext` + `DefaultSessionContext` +- `ServerRegistrationStore.Upsert` persists new fields +- `ServerSetupPanel.axaml` has text areas for per-request and default session context +- `SaveServerRegistrationRequest/Handler` passes new fields through + +--- + +## Recent Changes (this session) + +### Commits (oldest → newest): +``` +2d2b678 Add server profiles: save connection details per host:port +41796c9 Make Enter key send message on Android +6384ba0 Add FR-19/20, TR-21/22: server profiles and mobile chat UX requirements +5ceb870 Fix Android Enter-to-send, icon-only Send button, compact button padding +bfdc4da CQRS server profile handlers + clear API key feature +032e786 Replace .NET splash screen logo with app icon +588fcd7 Fix Android soft keyboard Enter: show Send action instead of newline +``` + +### Key changes: +1. **Server profiles** — Full CQRS implementation with 3 request/handler pairs, tests, settings UI +2. **Android Enter-to-send** — `SetRawInputType(InputTypes.ClassText)` on native `AppCompatEditText` forces IME to show Send button instead of newline for multi-line Editor +3. **Compact mobile chat UI** — Removed icon label from input area, shrunk session tab close buttons (24×24), compact session card buttons, toolbar reduced to Connect/Disconnect only +4. **Splash screen** — Replaced .NET logo with app's own icon (`appiconfg.svg`) +5. **Default port** — Changed from 5244 to 5243 (Linux/Docker is more common deployment) + +--- + +## Key Files Reference + +### Mobile App +| File | Purpose | +|------|---------| +| `MainPage.xaml` | Chat screen — connection view, session tabs, message list, input area | +| `MainPage.xaml.cs` | Platform-specific keyboard: Android `EditorAction`+`SetRawInputType`, Windows Ctrl+Enter | +| `SettingsPage.xaml` | Server profiles list + edit form + Clear API Key | +| `ViewModels/MainPageViewModel.cs` | Main VM, default port 5243, CQRS dispatch via `RunAsync()` | +| `ViewModels/SettingsPageViewModel.cs` | Settings VM, CQRS dispatch for Save/Delete/ClearApiKey | +| `MauiProgram.cs` | DI registration hub — all handlers, services, ViewModels | +| `Handlers/ClearServerApiKeyHandler.cs` | Clears API key from stored profile | +| `Resources/Splash/splash.svg` | Splash screen (now app icon, not .NET logo) | +| `Resources/Styles/Styles.xaml` | Global Button style: padding `12,6`, min height `36` | + +### Service +| File | Purpose | +|------|---------| +| `Program.cs` | Kestrel setup, dual-port (gRPC + web), `/pair` routes | +| `Services/AgentGatewayService.cs` | gRPC service: Connect, SendMessage, SetPairingUsers | +| `Web/PairingHtml.cs` | Login form + key page HTML generation | + +### Desktop +| File | Purpose | +|------|---------| +| `Infrastructure/ServerRegistration.cs` | Server model with PerRequestContext + DefaultSessionContext | +| `Views/Panels/ServerSetupPanel.axaml` | Per-request context + default session context text fields | + +### Shared +| File | Purpose | +|------|---------| +| `App.Logic/ServerProfile.cs` | ServerProfile model + IServerProfileStore interface | +| `App.Logic/Cqrs/` | IRequest, IRequestHandler, IRequestDispatcher, CommandResult | + +--- + +## Outstanding Work + +### Active bug: +- **`debug-server-agents`** — Agents aren't running on the server. Need to investigate service logs, gRPC session lifecycle, agent spawning logic. This is the highest priority item. + +### Stale SQL todos (already implemented, can be marked done): +The following SQL todo items are from the `SetPairingUsers` feature that was completed in prior sessions. They can be cleaned up: +- `proto-update`, `proto-gen`, `service-impl`, `client-impl`, `request-handler` +- `app-registration`, `dialog-infra`, `mainwindow-xaml`, `viewmodel-updates` +- `test-file`, `test-stubs`, `build-verify` + +### Remaining from server profiles plan: +- `desktop-profile-align` (in_progress) — Desktop PerRequestContext/DefaultSessionContext fields were added. May need further testing. +- `profile-tests` (pending) — Additional unit tests for profile store CRUD and auto-save. Basic handler tests exist (7 in `ServerProfileHandlerTests.cs`), but more coverage could be added. + +--- + +## Known Issues / Gotchas + +### Android APK build +- **Release config fails** with `XAGNM7009` (native code gen ARM64 issue in .NET 10 Android SDK 36.1.30). Use **Debug** config with `-p:EmbedAssembliesIntoApk=true`. +- Requires Linux Android SDK: `-p:AndroidSdkDirectory=/home/sharpninja/Android/Sdk` + +### Android soft keyboard Enter key +- MAUI `Editor` = multi-line `AppCompatEditText`. Android ignores `ImeOptions` for multi-line inputs. +- Fix: `SetRawInputType(InputTypes.ClassText)` tells IME to show Send button while view still wraps text. +- `EditorAction` event handles `ImeAction.Send`, `Done`, and `Unspecified` (hardware Enter). + +### Desktop build on Linux (NETSDK1045) +- `build-desktop-dotnet9.sh` shows NETSDK1045 because .NET 9 SDK doesn't know `net10.0`. +- Workaround: Build desktop directly with `dotnet build src/RemoteAgent.Desktop/RemoteAgent.Desktop.csproj -c Release` (the .NET 10 SDK can build net9.0 targets). + +### Test project linking +- `RemoteAgent.App.Tests` can't reference the MAUI app project. Source files are linked via `` in the `.csproj`. +- `MauiStubs.cs` provides minimal `Microsoft.Maui.Controls.Command` stub. +- **When adding new request/handler files, you must add corresponding `` links.** + +### `IOptions` vs `IOptionsMonitor` +- All `/pair` routes use `IOptionsMonitor.CurrentValue` so settings reload after `SetPairingUsers` writes `appsettings.json`. + +### ADB on WSL2 +- Windows ADB: `/mnt/c/Users/kingd/platform-tools-windows/platform-tools/adb.exe` +- APK paths via `wslpath -w "$APK"` for Windows-compatible paths +- Emulator: `emulator-5556` +- Activity: `crc6411305d2bb8acc544.MainActivity` +- Package: `com.companyname.remoteagent.app` +- Always `force-stop` before relaunching to avoid destroyed-activity crashes + +### Warnings as errors +- `TreatWarningsAsErrors=true` in `Directory.Build.props` +- Fix nullability issues, don't suppress warnings without approval + +### Requirements matrix script +- `scripts/generate-requirements-matrix.sh` has pre-existing awk syntax errors +- Matrix entries in `docs/requirements-test-coverage.md` must be added manually + +--- + +## Recent Commit History + +``` +588fcd7 Fix Android soft keyboard Enter: show Send action instead of newline +032e786 Replace .NET splash screen logo with app icon +bfdc4da CQRS server profile handlers + clear API key feature +5ceb870 Fix Android Enter-to-send, icon-only Send button, compact button padding +6384ba0 Add FR-19/20, TR-21/22: server profiles and mobile chat UX requirements +41796c9 Make Enter key send message on Android +2d2b678 Add server profiles: save connection details per host:port +22da942 Hide connection card in chat view when already connected +b3d01ac Hide navigation until connection and session are established +a6e698c Migrate GetSessionCapacityAsync to gRPC, remove dead HTTP helpers +9ef7799 Hardcode server connection mode on Android, remove mode selector dialog +5b4f109 docs: add FR-17/18 and TR-19/20 for device pairing, API key management, and desktop UX +a7add4c docs: update session handoff 2026-02-20 +1dfe673 Replace QR camera scanner with server login webview for pairing +0a3e7e7 Fix: generate QR code server-side (embedded PNG) instead of CDN JS +324f229 Style: global 4px margin on all interactive/display controls in Avalonia app +0b491de UX: SelectableTextBlock everywhere in Avalonia; simplify mobile login UI; persist ApiKey on connect +25a470e Fix: use Shell.Current.Navigation + MainThread for QR scanner modal push +ddaa2f1 Fix: forward ApiKey to server info and capacity checks in ConnectMobileSessionHandler +2c30fd5 Feat: generate and return API key when saving pairing user +``` diff --git a/docs/REPOSITORY_RULES.md b/docs/REPOSITORY_RULES.md index 19e86bd..5312184 100644 --- a/docs/REPOSITORY_RULES.md +++ b/docs/REPOSITORY_RULES.md @@ -1,5 +1,19 @@ # Repository rules +## UI Buttons and CQRS + +**Every button added to the Desktop UI must be backed by a CQRS request/handler pair with complete tests.** + +- **Request:** Create a `*Request` record in `src/RemoteAgent.Desktop/Requests/` implementing `IRequest`. +- **Handler:** Create a matching `*Handler` class in `src/RemoteAgent.Desktop/Handlers/` implementing `IRequestHandler`. +- **Register:** Add the handler to `ConfigureServices` in `App.axaml.cs` as a transient `IRequestHandler`. +- **ViewModel:** The command bound to the button must dispatch via `IRequestDispatcher`. No logic that belongs in the handler may live directly in the ViewModel. +- **Handler tests:** Add `*HandlerTests.cs` in `tests/RemoteAgent.Desktop.UiTests/Handlers/` covering at least success, failure/empty, and any meaningful edge cases. Use `[Fact]` for pure logic tests and `[AvaloniaFact]` only when Avalonia UI context is required. +- **UI tests:** If the button interaction involves ViewModel state changes observable from the UI, add corresponding tests in `tests/RemoteAgent.Desktop.UiTests/`. +- **Stubs:** Any new infrastructure interface introduced for the handler must have a `Null*` stub added to `SharedHandlerTestStubs.cs`. + +This rule exists so that all button behaviour is independently testable, auditable through the request pipeline, and consistent with the established CQRS architecture. + ## Warnings as errors - **All warnings are treated as errors.** The build is configured with `TreatWarningsAsErrors=true` (see `Directory.Build.props`). The CI build fails on any warning. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..b809d63 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,84 @@ +# Frequently Asked Questions + +## Service Management + +### How do I start the service when systemd is not available? + +This applies to environments such as WSL (Windows Subsystem for Linux) where +systemd is not the active init system (i.e. `/run/systemd/system` does not +exist). + +#### Automatic start on install + +When you install the `remote-agent-service` .deb package on a non-systemd +system, the `postinst` script automatically starts the service using +`start-stop-daemon` and writes a PID file to `/run/remote-agent.pid`. + +On WSL it also registers an auto-start entry in `/etc/wsl.conf`: + +```ini +[boot] +command = "su -s /bin/sh remote-agent /usr/lib/remote-agent/service/wsl-start.sh" +``` + +After this is written, the service starts automatically every time the WSL +instance launches. + +> **Tip:** For full `systemctl` support in WSL, add `systemd = true` under +> `[boot]` in `/etc/wsl.conf`, then run `wsl --shutdown` to restart the +> distro. + +#### Starting manually + +Use `remote-agent-ctl`, installed with the package: + +```bash +sudo remote-agent-ctl start +``` + +The script works with or without systemd — it detects the active init system +automatically and delegates to `systemctl` when available, or falls back to +`start-stop-daemon` directly. + +You can also stop, restart, and check status with the same script: + +```bash +sudo remote-agent-ctl stop +sudo remote-agent-ctl restart +sudo remote-agent-ctl status +``` + +Environment overrides in `/etc/remote-agent/environment` (e.g. +`ASPNETCORE_URLS`) are sourced automatically by the script when running without +systemd. + +#### Checking whether the service is running + +```bash +_pid=$(cat /run/remote-agent.pid 2>/dev/null) +kill -0 "$_pid" 2>/dev/null && echo "running (pid $_pid)" || echo "not running" +``` + +#### Viewing logs + +```bash +tail -f /var/log/remote-agent/service.log +``` + +Errors are written to the same file. A separate `.err` file is used only when +the service is started via the `daemonize` fallback (present on some +distributions such as Pengwin): + +```bash +tail -f /var/log/remote-agent/service.err +``` + +#### Configuration + +Runtime configuration is loaded from `/etc/remote-agent/appsettings.json`. +Environment overrides (e.g. `ASPNETCORE_ENVIRONMENT`, `ASPNETCORE_URLS`) are +read from `/etc/remote-agent/environment`. Both files are preserved during +package upgrades. + +The service runs as the `remote-agent` system user with data stored in +`/var/lib/remote-agent/` and logs in `/var/log/remote-agent/`. diff --git a/docs/functional-requirements.md b/docs/functional-requirements.md index d15d958..8feeb6e 100644 --- a/docs/functional-requirements.md +++ b/docs/functional-requirements.md @@ -24,8 +24,8 @@ - **FR-2.2** Agent output (stdout/stderr) shall be **streamed to the app in real time** and displayed in the chat. - **FR-2.3** Agent output in the chat shall be **formatted with a markdown parser** (e.g. bold, code, lists) so that Cursor output is readable and structured. - **FR-2.4** The user shall be able to **connect** to the service (host/port) and **disconnect**; connecting shall start a session with the agent; disconnecting shall stop the session and the agent. -- **FR-2.5** Chat text entry shall support **multi-line input**; pressing **Enter/Return** shall insert a newline in the request text. -- **FR-2.6** On desktop clients, pressing **Ctrl+Enter** in the chat editor shall submit the request. +- **FR-2.5** Chat text entry shall support **multi-line input** on desktop; on mobile, Enter sends the message. +- **FR-2.6** On desktop clients, pressing **Ctrl+Enter** in the chat editor shall submit the request; on mobile clients, pressing **Enter** shall submit the request. - **FR-2.7** On mobile clients, session establishment shall begin in a **dedicated connection view** and transition to the **chat view** after successful connection. *See:* [TR-3](technical-requirements.md#3-service-architecture), [TR-4](technical-requirements.md#4-protocol-grpc), [TR-5](technical-requirements.md#5-app-architecture). @@ -177,3 +177,52 @@ ## 16. Test execution policy - **FR-16.1** Integration tests shall be executable on demand through an explicit isolated workflow or script and shall not run as part of default build-and-release pipeline executions. + +--- + +## 17. Device pairing and API key management + +- **FR-17.1** The system shall support a **device pairing flow** that securely provisions a mobile device with connection details and an API key for the server. +- **FR-17.2** An administrator shall be able to **set pairing credentials** (username and password) from the desktop management app; the service shall hash and store them in configuration. +- **FR-17.3** When pairing credentials are saved, the service shall **generate a cryptographically random API key**, store it in server configuration, and **return it** to the desktop app. +- **FR-17.4** The service shall expose a **web-based login page** at `/pair` on the HTTP/1 web port (computed as `"1" + gRPC port`) so that a mobile device can authenticate with the pairing credentials. +- **FR-17.5** After successful login, the service shall display the API key, a **server-generated QR code** (no external CDN dependency), and a **deep-link button** (`remoteagent://pair?key=…&host=…&port=…`) that carries connection details. +- **FR-17.6** The mobile app shall provide a **Login button** that opens the server's `/pair` page in an in-app WebView; after successful authentication the app shall **automatically extract the deep-link URI** from the rendered page and populate host, port, and API key. + - **FR-17.6.1** The Login button shall be **disabled** until a server host/IP is entered. + - **FR-17.6.2** The Login button shall be **disabled** when an API key is already stored (pairing already completed). +- **FR-17.7** The mobile app's Connect button shall be **disabled** until a valid API key is stored (i.e., pairing must precede connection). +- **FR-17.8** The API key shall be **persisted** on the mobile device alongside the server host and port after a successful connection. + +*See:* [TR-19](technical-requirements.md#19-device-pairing-and-api-key-management). + +--- + +## 18. Desktop UX refinements + +- **FR-18.1** All text display controls in Avalonia desktop views shall use **SelectableTextBlock** (instead of `TextBlock`) so users can select and copy displayed text. +- **FR-18.2** All interactive and display controls in the Avalonia desktop app shall have a uniform **4px margin** applied via a global style. + +*See:* [TR-20](technical-requirements.md#20-desktop-ux-refinements). + +--- + +## 19. Server profiles and persistent connection settings + +- **FR-19.1** Both mobile and desktop apps shall **persist server connection details** (host, port, API key, display name) after a successful connection, keyed by **host:port**. +- **FR-19.2** The Settings page (mobile) and Server Setup panel (desktop) shall display a **list of saved server profiles** with the ability to **add, edit, and remove** entries. +- **FR-19.3** Each saved server profile shall store a configurable **per-request context** (text prepended to every chat message) and a **default session context** (context seeded into new sessions). +- **FR-19.4** Connecting to a previously saved server shall **auto-load** the stored per-request context into the workspace (without overriding user-entered values). +- **FR-19.5** Saving or updating a server profile shall not affect **existing active sessions**; changes apply only to subsequent connections. + +*See:* [TR-21](technical-requirements.md#21-server-profiles-and-persistent-connection-settings). + +--- + +## 20. Mobile chat UX + +- **FR-20.1** On mobile clients, pressing **Enter** in the chat input shall **send the message** (submit the request). +- **FR-20.2** On mobile clients, the **connection mode shall be fixed to "server"**; no mode-selection dialog shall be shown. +- **FR-20.3** Navigation items (Settings, Account, MCP Registry) shall be **hidden until a connection is established**; the connection card shall be **hidden once connected**. +- **FR-20.4** All client-to-server API calls shall use **gRPC exclusively**; no REST/HTTP fallback endpoints shall be used by the mobile client. + +*See:* [TR-22](technical-requirements.md#22-mobile-chat-ux). diff --git a/docs/requirements-completion-summary.md b/docs/requirements-completion-summary.md index 0932082..76944ad 100644 --- a/docs/requirements-completion-summary.md +++ b/docs/requirements-completion-summary.md @@ -80,6 +80,18 @@ Requirement IDs in the tables link to the corresponding section in [Functional r | [**FR-15.1**](functional-requirements.md#15-connection-protection) | Configurable connection/message rate limiting | **Done** | `ConnectionProtectionService` enforces sliding-window limits | | [**FR-15.2**](functional-requirements.md#15-connection-protection) | Detect DoS patterns and temporarily throttle/block peers | **Done** | DoS detection cooldown + blocked-peer behavior with structured events | | [**FR-16.1**](functional-requirements.md#16-test-execution-policy) | Integration tests run on-demand and remain isolated from default pipeline | **Done** | `scripts/test-integration.sh` + isolated workflow (`integration-tests.yml`) | +| [**FR-17.1**](functional-requirements.md#17-device-pairing-and-api-key-management) | Device pairing flow provisions mobile with connection details and API key | **Done** | Desktop SetPairingUser → service generates key → `/pair` web login → mobile extracts deep link | +| [**FR-17.2**](functional-requirements.md#17-device-pairing-and-api-key-management) | Admin sets pairing credentials from desktop app | **Done** | Desktop toolbar button → `SetPairingUserHandler` → `SetPairingUsers` gRPC RPC | +| [**FR-17.3**](functional-requirements.md#17-device-pairing-and-api-key-management) | Service generates random API key on pairing credential save | **Done** | `AgentGatewayService.SetPairingUsers` generates 32-byte hex key via `RandomNumberGenerator` | +| [**FR-17.4**](functional-requirements.md#17-device-pairing-and-api-key-management) | Web-based login page at `/pair` on web port | **Done** | `PairingHtml.LoginPage` served on `"1" + gRPCport` (e.g. 15244) | +| [**FR-17.5**](functional-requirements.md#17-device-pairing-and-api-key-management) | After login: API key, server-side QR code, deep-link button displayed | **Done** | `PairingHtml.KeyPage` renders key + `QRCoder` PNG + `remoteagent://pair?…` anchor | +| [**FR-17.6**](functional-requirements.md#17-device-pairing-and-api-key-management) | Mobile Login button opens `/pair` WebView and auto-extracts deep link | **Done** | `PairLoginPage` WebView → JS `querySelector('a.btn').getAttribute('href')` → `ScanQrCodeHandler.ParseAndApply` | +| [**FR-17.6.1**](functional-requirements.md#17-device-pairing-and-api-key-management) | Login disabled until host entered | **Done** | `ScanQrCodeCommand` CanExecute includes `!string.IsNullOrWhiteSpace(_host)` | +| [**FR-17.6.2**](functional-requirements.md#17-device-pairing-and-api-key-management) | Login disabled when API key already stored | **Done** | `ScanQrCodeCommand` CanExecute includes `!HasApiKey` | +| [**FR-17.7**](functional-requirements.md#17-device-pairing-and-api-key-management) | Connect disabled until API key stored | **Done** | `ConnectCommand` CanExecute: `!_gateway.IsConnected && HasApiKey` | +| [**FR-17.8**](functional-requirements.md#17-device-pairing-and-api-key-management) | API key persisted on mobile after successful connection | **Done** | `ConnectMobileSessionHandler` saves `PrefApiKey` to preferences on connect | +| [**FR-18.1**](functional-requirements.md#18-desktop-ux-refinements) | All Avalonia text displays use SelectableTextBlock | **Done** | All 16 `.axaml` view files: `TextBlock` → `SelectableTextBlock` | +| [**FR-18.2**](functional-requirements.md#18-desktop-ux-refinements) | Uniform 4px margin on all Avalonia controls | **Done** | Global style in `App.axaml` targeting Button, TextBox, ComboBox, CheckBox, ListBox, SelectableTextBlock, TabControl | --- @@ -199,6 +211,17 @@ Requirement IDs in the tables link to the corresponding section in [Functional r | [**TR-18.2**](technical-requirements.md#18-ui-commandevent-cqrs-testability) | Command/query/event handlers are unit-testable independent of UI frameworks | **Done** | 218 handler unit tests across Desktop and Mobile; handlers tested with stub dependencies, no UI framework required | | [**TR-18.3**](technical-requirements.md#18-ui-commandevent-cqrs-testability) | UI pipelines support mockable behavior injection for known outcomes/failures | **Done** | `IRequestDispatcher` is the sole pipeline entry point; `ServiceProviderRequestDispatcher` provides Debug-level entry/exit logging with CorrelationId tracing; all dependencies are interface-backed | | [**TR-18.4**](technical-requirements.md#18-ui-commandevent-cqrs-testability) | UI tests substitute mocked handlers and validate success/failure UI behavior | **Done** | Handler tests use `StubCapacityClient`, `NullDispatcher`, `TestRequestDispatcher`, and other injectable stubs to validate both success and failure paths; 240 tests covering all handler paths | +| [**TR-19.1**](technical-requirements.md#19-device-pairing-and-api-key-management) | `SetPairingUsers` RPC hashes passwords, generates API key, returns it | **Done** | `AgentGatewayService.SetPairingUsers` → SHA-256 hash + `RandomNumberGenerator.GetBytes(32)` hex key → `appsettings.json` + `GeneratedApiKey` response field | +| [**TR-19.2**](technical-requirements.md#19-device-pairing-and-api-key-management) | `/pair` endpoints use `IOptionsMonitor` for live config reload | **Done** | All three `/pair` lambdas use `IOptionsMonitor.CurrentValue` | +| [**TR-19.3**](technical-requirements.md#19-device-pairing-and-api-key-management) | Secondary HTTP/1+2 web port (`"1" + gRPC port`) for browser endpoints | **Done** | Kestrel dual-port: gRPC HTTP/2-only + web HTTP/1+2; `/pair` routes use `RequireHost` | +| [**TR-19.4**](technical-requirements.md#19-device-pairing-and-api-key-management) | Server-side QR code via `QRCoder.PngByteQRCode`, no CDN | **Done** | `PairingHtml.GenerateQrPngImg` → base64 `` data URL | +| [**TR-19.5**](technical-requirements.md#19-device-pairing-and-api-key-management) | Deep-link URI: `remoteagent://pair?key=…&host=…&port={gRPCport}` | **Done** | `/pair/key` builds deep link with `context.Request.Host` and gRPC port | +| [**TR-19.6**](technical-requirements.md#19-device-pairing-and-api-key-management) | `PairLoginPage` WebView extracts deep link via JS on `/pair/key` navigation | **Done** | `PairLoginPage.OnNavigated` → `EvaluateJavaScriptAsync` → `querySelector('a.btn').getAttribute('href')` → strip JSON quotes → TCS | +| [**TR-19.7**](technical-requirements.md#19-device-pairing-and-api-key-management) | `ScanQrCodeHandler` validates host, builds web port, parses deep link | **Done** | Validates host not empty; `loginUrl = http://{host}:1{port}/pair`; `ParseAndApply` extracts key/host/port from URI | +| [**TR-19.8**](technical-requirements.md#19-device-pairing-and-api-key-management) | Desktop `SetPairingUsersAsync` returns `Task` (generated key) | **Done** | `IServerCapacityClient.SetPairingUsersAsync` → `Task`; handler applies key to `Workspace.ApiKey` | +| [**TR-19.9**](technical-requirements.md#19-device-pairing-and-api-key-management) | `IQrCodeScanner.ScanAsync` accepts `loginUrl` parameter | **Done** | Interface signature: `Task ScanAsync(string loginUrl)` | +| [**TR-20.1**](technical-requirements.md#20-desktop-ux-refinements) | All Avalonia `TextBlock` replaced with `SelectableTextBlock` | **Done** | All 16 desktop `.axaml` view files updated | +| [**TR-20.2**](technical-requirements.md#20-desktop-ux-refinements) | Global 4px margin style on interactive Avalonia controls | **Done** | `App.axaml` style targets Button, TextBox, ComboBox, CheckBox, ListBox, SelectableTextBlock, TabControl | --- @@ -206,8 +229,8 @@ Requirement IDs in the tables link to the corresponding section in [Functional r | Category | Done | Partial | Not started | |----------|------|--------|-------------| -| **Functional (FR)** | 67 | 0 | 0 | -| **Technical (TR)** | 112 | 0 | 0 | +| **Functional (FR)** | 80 | 0 | 0 | +| **Technical (TR)** | 124 | 0 | 0 | **Not started (FR):** None. @@ -215,4 +238,4 @@ Requirement IDs in the tables link to the corresponding section in [Functional r --- -*Generated from `docs/functional-requirements.md`, `docs/technical-requirements.md`, and the current codebase. Last refreshed: TR-18.1–TR-18.4 marked Done following completion of the MVVM + CQRS refactor (32 Desktop handlers, 17 Mobile handlers, 218 unit tests, ServerWorkspaceViewModel decomposed into 6 sub-VMs, IRequestDispatcher with Debug-level CorrelationId tracing).* +*Generated from `docs/functional-requirements.md`, `docs/technical-requirements.md`, and the current codebase. Last refreshed: FR-17 (device pairing + API key management), FR-18 (desktop UX refinements), TR-19 (pairing infrastructure), TR-20 (desktop UX) marked Done following web-based login flow, server-side QR code, SelectableTextBlock, and global margin implementation.* diff --git a/docs/requirements-test-coverage.md b/docs/requirements-test-coverage.md index addfe70..4e4009e 100644 --- a/docs/requirements-test-coverage.md +++ b/docs/requirements-test-coverage.md @@ -2,6 +2,8 @@ This document maps every requirement ID in `docs/functional-requirements.md` (FR) and `docs/technical-requirements.md` (TR) to current automated test coverage. +**Note:** This file is auto-generated by `scripts/generate-requirements-matrix.sh`. Do not edit manually. + Coverage status values: - `Covered`: direct automated test coverage exists. - `Partial`: some behavior is covered, but not the full requirement scope. @@ -11,74 +13,100 @@ Coverage status values: | Requirement | Coverage | Tests | |---|---|---| -| FR-1.1 | Partial | `HostBootstrapSmokeTests.RootEndpoint_RespondsQuickly` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`), `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | -| FR-1.2 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `AgentGatewayServiceIntegrationTests_NoCommand.Connect_WhenNoAgentCommandConfigured_ReceivesSessionErrorEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`) | -| FR-1.3 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | -| FR-1.4 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | -| FR-1.5 | Partial | `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-1.6 | Partial | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`), `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | -| FR-2.1 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | -| FR-2.2 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | +| FR-1.1 | Covered | `HostBootstrapSmokeTests.*` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`), `CheckLocalServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckLocalServerHandlerTests.cs`) | +| FR-1.2 | Covered | `AgentGatewayServiceIntegrationTests_NoCommand.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`), `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `HostBootstrapSmokeTests.*` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`), `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `ApplyLocalServerActionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ApplyLocalServerActionHandlerTests.cs`) | +| FR-1.3 | Covered | `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | +| FR-1.4 | Covered | `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | +| FR-1.5 | Covered | `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-1.6 | None | None | +| FR-2.1 | Covered | `SendDesktopMessageHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SendDesktopMessageHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | +| FR-2.2 | Covered | `SendDesktopMessageHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SendDesktopMessageHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | | FR-2.3 | Covered | `MarkdownFormatTests.*` (`tests/RemoteAgent.App.Tests/MarkdownFormatTests.cs`), `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | -| FR-2.4 | Covered | `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | +| FR-2.4 | Covered | `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | | FR-2.5 | None | None | -| FR-2.6 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-2.7 | Covered | `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | +| FR-2.6 | None | None | +| FR-2.7 | None | None | | FR-3.1 | Covered | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | | FR-3.2 | None | None | | FR-3.3 | None | None | -| FR-3.4 | Partial | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | +| FR-3.4 | None | None | | FR-4.1 | Covered | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | -| FR-4.2 | Covered | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | +| FR-4.2 | None | None | | FR-5.1 | None | None | | FR-5.2 | None | None | | FR-5.3 | None | None | | FR-6.1 | None | None | | FR-6.2 | None | None | -| FR-7.1 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | -| FR-7.2 | Covered | `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`), `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | -| FR-8.1 | Covered | `PluginConfigurationServiceTests.*` (`tests/RemoteAgent.Service.Tests/PluginConfigurationServiceTests.cs`), `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | -| FR-9.1 | Partial | `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | +| FR-7.1 | Covered | `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | +| FR-7.2 | Covered | `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | +| FR-8.1 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`), `PluginConfigurationServiceTests.*` (`tests/RemoteAgent.Service.Tests/PluginConfigurationServiceTests.cs`) | +| FR-9.1 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | | FR-9.2 | None | None | | FR-10.1 | None | None | -| FR-11.1 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-11.1 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | FR-11.1.1 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-11.1.2 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | -| FR-11.1.3 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-11.1.2 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | +| FR-11.1.3 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | FR-11.1.3.1 | None | None | | FR-11.1.3.2 | None | None | -| FR-12.1 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.1.1 | Partial | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.1.2 | Partial | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.1.3 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.1.4 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.1.5 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.2 | Partial | `StructuredLogStoreTests.Query_ShouldApplyFilterCriteria` (`tests/RemoteAgent.Desktop.UiTests/StructuredLogStoreTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-12.3 | Covered | `StructuredLogStoreTests.Query_ShouldApplyFilterCriteria` (`tests/RemoteAgent.Desktop.UiTests/StructuredLogStoreTests.cs`) | -| FR-12.4 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-12.5 | Partial | `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | -| FR-12.6 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-12.7 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-12.8 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-12.9 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.10 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| FR-12.11 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `StructuredLogStoreTests.Query_ShouldApplyFilterCriteria` (`tests/RemoteAgent.Desktop.UiTests/StructuredLogStoreTests.cs`) | -| FR-12.12 | Not covered | Pending implementation of management app log view | -| FR-13.1 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `SessionCapacityServiceTests.MarkSessionAbandoned_ShouldTrackAndClearOnRegister` (`tests/RemoteAgent.Service.Tests/SessionCapacityServiceTests.cs`) | -| FR-13.2 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`) | -| FR-13.3 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-13.4 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`) | -| FR-13.5 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `AuthUserServiceTests.*` (`tests/RemoteAgent.Service.Tests/AuthUserServiceTests.cs`) | -| FR-13.6 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-13.7 | Covered | `SessionCapacityServiceTests_CapacityLimits.*` (`tests/RemoteAgent.Service.Tests/SessionCapacityServiceTests_CapacityLimits.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-13.8 | Covered | `SessionCapacityServiceTests_CapacityLimits.*` (`tests/RemoteAgent.Service.Tests/SessionCapacityServiceTests_CapacityLimits.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-14.1 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-14.2 | Covered | `PromptTemplateEngineTests.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests.cs`), `PromptTemplateEngineTests_EdgeCases.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests_EdgeCases.cs`) | -| FR-14.3 | Partial | `PromptTemplateEngineTests.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| FR-14.4 | Partial | `PromptTemplateEngineTests.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-12.1 | Covered | `SetManagementSectionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetManagementSectionHandlerTests.cs`), `OpenNewSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/OpenNewSessionHandlerTests.cs`), `ConnectionSettingsDialogViewModelTests.*` (`tests/RemoteAgent.Desktop.UiTests/ViewModels/ConnectionSettingsDialogViewModelTests.cs`) | +| FR-12.1.1 | None | None | +| FR-12.1.2 | None | None | +| FR-12.1.3 | None | None | +| FR-12.1.4 | None | None | +| FR-12.1.5 | None | None | +| FR-12.2 | Covered | `ConnectionSettingsDialogViewModelTests.*` (`tests/RemoteAgent.Desktop.UiTests/ViewModels/ConnectionSettingsDialogViewModelTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-12.3 | Covered | `TerminateDesktopSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateDesktopSessionHandlerTests.cs`) | +| FR-12.4 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-12.5 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`), `DeleteMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteMcpServerHandlerTests.cs`), `SavePluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePluginsHandlerTests.cs`), `SaveAgentMcpMappingHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAgentMcpMappingHandlerTests.cs`), `SaveMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveMcpServerHandlerTests.cs`), `RefreshMcpRegistryHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshMcpRegistryHandlerTests.cs`), `RefreshPluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPluginsHandlerTests.cs`) | +| FR-12.6 | Covered | `SeedSessionContextHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SeedSessionContextHandlerTests.cs`), `DeletePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeletePromptTemplateHandlerTests.cs`), `RefreshPromptTemplatesHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPromptTemplatesHandlerTests.cs`), `SavePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePromptTemplateHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-12.7 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-12.8 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-12.9 | Covered | `RemoveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RemoveServerRegistrationHandlerTests.cs`), `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`) | +| FR-12.10 | None | None | +| FR-12.11 | Covered | `ExpandStatusLogPanelHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ExpandStatusLogPanelHandlerTests.cs`), `StartLogMonitoringHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/StartLogMonitoringHandlerTests.cs`) | +| FR-12.12 | Covered | `ClearAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ClearAppLogHandlerTests.cs`), `SaveAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAppLogHandlerTests.cs`) | +| FR-12.12.1 | None | None | +| FR-12.12.2 | None | None | +| FR-12.12.3 | None | None | +| FR-12.12.4 | None | None | +| FR-12.12.5 | None | None | +| FR-13.1 | Covered | `RefreshOpenSessionsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshOpenSessionsHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-13.2 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`), `RefreshSecurityDataHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshSecurityDataHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-13.3 | Covered | `TerminateDesktopSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateDesktopSessionHandlerTests.cs`), `TerminateOpenServerSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateOpenServerSessionHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-13.4 | Covered | `BanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/BanPeerHandlerTests.cs`), `UnbanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/UnbanPeerHandlerTests.cs`), `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`), `RefreshSecurityDataHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshSecurityDataHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-13.5 | Covered | `DeleteAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteAuthUserHandlerTests.cs`), `RefreshAuthUsersHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshAuthUsersHandlerTests.cs`), `SaveAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAuthUserHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-13.6 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-13.7 | Covered | `CheckSessionCapacityHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckSessionCapacityHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-13.8 | Covered | `CheckSessionCapacityHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckSessionCapacityHandlerTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-14.1 | Covered | `PromptTemplateServiceTests.*` (`tests/RemoteAgent.Service.Tests/PromptTemplateServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-14.2 | Covered | `PromptTemplateServiceTests.*` (`tests/RemoteAgent.Service.Tests/PromptTemplateServiceTests.cs`), `PromptTemplateEngineTests.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests.cs`) | +| FR-14.3 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-14.4 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | FR-15.1 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | FR-15.2 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`) | -| FR-16.1 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `AgentGatewayServiceIntegrationTests_NoCommand.Connect_WhenNoAgentCommandConfigured_ReceivesSessionErrorEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-16.1 | Covered | `AgentGatewayServiceIntegrationTests_NoCommand.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| FR-17.1 | Partial | `ScanQrCodeHandlerTests.*` (`tests/RemoteAgent.App.Tests/ScanQrCodeHandlerTests.cs`), `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | +| FR-17.2 | Partial | `SetPairingUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetPairingUserHandlerTests.cs`) | +| FR-17.3 | None | None | +| FR-17.4 | None | None | +| FR-17.5 | None | None | +| FR-17.6 | Partial | `ScanQrCodeHandlerTests.*` (`tests/RemoteAgent.App.Tests/ScanQrCodeHandlerTests.cs`) | +| FR-17.6.1 | None | None | +| FR-17.6.2 | None | None | +| FR-17.7 | None | None | +| FR-17.8 | Partial | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | +| FR-18.1 | None | None | +| FR-18.2 | None | None | +| FR-19.1 | Partial | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`), `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`) | +| FR-19.2 | Partial | `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`), `RemoveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RemoveServerRegistrationHandlerTests.cs`) | +| FR-19.3 | Partial | `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`) | +| FR-19.4 | None | None | +| FR-19.5 | None | None | +| FR-20.1 | None | None | +| FR-20.2 | Covered | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | +| FR-20.3 | None | None | +| FR-20.4 | Covered | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | ## Technical Requirements (TR) @@ -86,36 +114,36 @@ Coverage status values: |---|---|---| | TR-1.1 | None | None | | TR-1.2 | None | None | -| TR-1.3 | None | None | +| TR-1.3 | Covered | `HostBootstrapSmokeTests.*` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`), `AgentOptionsTests.*` (`tests/RemoteAgent.Service.Tests/AgentOptionsTests.cs`) | | TR-1.4 | None | None | | TR-1.5 | None | None | -| TR-2.1 | Partial | `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | -| TR-2.1.1 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | +| TR-2.1 | None | None | +| TR-2.1.1 | None | None | | TR-2.1.2 | None | None | -| TR-2.2 | Partial | `HostBootstrapSmokeTests.RootEndpoint_RespondsQuickly` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`) | -| TR-2.3 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `AgentGatewayServiceIntegrationTests_NoCommand.Connect_WhenNoAgentCommandConfigured_ReceivesSessionErrorEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`) | +| TR-2.2 | Covered | `HostBootstrapSmokeTests.*` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`), `AgentOptionsTests.*` (`tests/RemoteAgent.Service.Tests/AgentOptionsTests.cs`) | +| TR-2.3 | Covered | `AgentGatewayServiceIntegrationTests_NoCommand.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`), `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | | TR-2.4 | None | None | -| TR-3.1 | Partial | `HostBootstrapSmokeTests.RootEndpoint_RespondsQuickly` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`) | -| TR-3.2 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | -| TR-3.3 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | -| TR-3.4 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | -| TR-3.5 | Partial | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | -| TR-3.6 | Partial | `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`) | -| TR-3.7 | Covered | `SessionCapacityServiceTests_CapacityLimits.*` (`tests/RemoteAgent.Service.Tests/SessionCapacityServiceTests_CapacityLimits.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-3.8 | Covered | `SessionCapacityServiceTests_CapacityLimits.*` (`tests/RemoteAgent.Service.Tests/SessionCapacityServiceTests_CapacityLimits.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-4.1 | Partial | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | -| TR-4.2 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | -| TR-4.3 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_CorrelationId.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_CorrelationId.cs`) | -| TR-4.4 | Covered | `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | -| TR-4.5 | Covered | `AgentGatewayServiceIntegrationTests_CorrelationId.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_CorrelationId.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-3.1 | Covered | `HostBootstrapSmokeTests.*` (`tests/RemoteAgent.Service.IntegrationTests/HostBootstrapSmokeTests.cs`), `AgentOptionsTests.*` (`tests/RemoteAgent.Service.Tests/AgentOptionsTests.cs`) | +| TR-3.2 | Covered | `AgentGatewayServiceIntegrationTests_NoCommand.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`), `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentOptionsTests.*` (`tests/RemoteAgent.Service.Tests/AgentOptionsTests.cs`) | +| TR-3.3 | Covered | `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | +| TR-3.4 | Covered | `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`), `AgentGatewayServiceIntegrationTests_Echo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | +| TR-3.5 | None | None | +| TR-3.6 | Covered | `LiteDbLocalStorageTests.*` (`tests/RemoteAgent.Service.Tests/LiteDbLocalStorageTests.cs`), `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`) | +| TR-3.7 | None | None | +| TR-3.8 | None | None | +| TR-4.1 | None | None | +| TR-4.2 | Covered | `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | +| TR-4.3 | Covered | `AgentGatewayServiceIntegrationTests_NoCommand.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`), `AgentGatewayServiceIntegrationTests_Stop.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | +| TR-4.4 | None | None | +| TR-4.5 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`), `AgentGatewayServiceIntegrationTests_CorrelationId.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_CorrelationId.cs`) | | TR-5.1 | Covered | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | -| TR-5.2 | Covered | `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | +| TR-5.2 | None | None | | TR-5.3 | Covered | `MarkdownFormatTests.*` (`tests/RemoteAgent.App.Tests/MarkdownFormatTests.cs`), `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | | TR-5.4 | None | None | | TR-5.5 | Covered | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`) | | TR-5.6 | None | None | -| TR-5.7 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-5.8 | Covered | `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | +| TR-5.7 | None | None | +| TR-5.8 | None | None | | TR-6.1 | None | None | | TR-6.2 | None | None | | TR-6.3 | None | None | @@ -127,73 +155,95 @@ Coverage status values: | TR-7.3.3 | None | None | | TR-7.3.4 | None | None | | TR-7.3.5 | None | None | -| TR-8.1 | Covered | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`), `AgentOptionsTests.*` (`tests/RemoteAgent.Service.Tests/AgentOptionsTests.cs`), `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`) | -| TR-8.2 | Covered | `MarkdownFormatTests.*` (`tests/RemoteAgent.App.Tests/MarkdownFormatTests.cs`), `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`), `AgentOptionsTests.*` (`tests/RemoteAgent.Service.Tests/AgentOptionsTests.cs`) | -| TR-8.3 | Covered | `AgentGatewayServiceIntegrationTests_NoCommand.Connect_WhenNoAgentCommandConfigured_ReceivesSessionErrorEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`), `AgentGatewayServiceIntegrationTests_Echo.Connect_StartThenSendText_ReceivesEchoFromAgent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Echo.cs`), `AgentGatewayServiceIntegrationTests_Stop.Connect_SendStop_ReceivesSessionStoppedEvent` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_Stop.cs`) | -| TR-8.4 | Partial | `ChatMessageTests.*` (`tests/RemoteAgent.App.Tests/ChatMessageTests.cs`), `AgentOptionsTests.*` (`tests/RemoteAgent.Service.Tests/AgentOptionsTests.cs`), `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-8.4.1 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-8.5 | Covered | `MobileConnectionUiTests.*` (`tests/RemoteAgent.Mobile.UiTests/MobileConnectionUiTests.cs`) | -| TR-8.6 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | +| TR-8.1 | None | None | +| TR-8.2 | None | None | +| TR-8.3 | Covered | `AgentGatewayServiceIntegrationTests_NoCommand.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_NoCommand.cs`) | +| TR-8.4 | None | None | +| TR-8.4.1 | None | None | +| TR-8.5 | None | None | +| TR-8.6 | None | None | | TR-9.1 | None | None | | TR-9.2 | None | None | | TR-9.3 | None | None | -| TR-10.1 | Covered | `PluginConfigurationServiceTests.*` (`tests/RemoteAgent.Service.Tests/PluginConfigurationServiceTests.cs`), `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | -| TR-10.2 | Covered | `PluginConfigurationServiceTests.*` (`tests/RemoteAgent.Service.Tests/PluginConfigurationServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-11.1 | Covered | `LiteDbLocalStorageTests.SessionExists_ReturnsTrue_WhenEntriesExist` (`tests/RemoteAgent.Service.Tests/LiteDbLocalStorageTests.cs`), `StructuredLogStoreTests.Query_ShouldApplyFilterCriteria` (`tests/RemoteAgent.Desktop.UiTests/StructuredLogStoreTests.cs`) | +| TR-10.1 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`), `PluginConfigurationServiceTests.*` (`tests/RemoteAgent.Service.Tests/PluginConfigurationServiceTests.cs`) | +| TR-10.2 | Covered | `PluginConfigurationServiceTests.*` (`tests/RemoteAgent.Service.Tests/PluginConfigurationServiceTests.cs`) | +| TR-11.1 | Covered | `LiteDbLocalStorageTests.*` (`tests/RemoteAgent.Service.Tests/LiteDbLocalStorageTests.cs`) | | TR-11.2 | None | None | | TR-11.3 | None | None | | TR-12.1 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | TR-12.1.1 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-12.1.2 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | -| TR-12.1.3 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-12.2 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | -| TR-12.2.1 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-12.1.2 | None | None | +| TR-12.1.3 | None | None | +| TR-12.2 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | +| TR-12.2.1 | None | None | | TR-12.2.2 | None | None | | TR-13.1 | Covered | `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`) | -| TR-13.2 | Covered | `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-13.2 | Covered | `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`) | | TR-13.3 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-13.4 | Covered | `StructuredLogStoreTests.Query_ShouldApplyFilterCriteria` (`tests/RemoteAgent.Desktop.UiTests/StructuredLogStoreTests.cs`) | -| TR-13.5 | Covered | `StructuredLogStoreTests.Query_ShouldApplyFilterCriteria` (`tests/RemoteAgent.Desktop.UiTests/StructuredLogStoreTests.cs`) | -| TR-13.6 | Partial | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.0 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.1 | Partial | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.2 | Partial | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.3 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.4 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.5 | Partial | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.6 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.7 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.8 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.9 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.1.10 | Covered | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.2 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-14.3 | Covered | `AgentGatewayServiceIntegrationTests_GetServerInfo.GetServerInfo_ReturnsVersionAndCapabilities` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_GetServerInfo.cs`) | +| TR-13.4 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-13.5 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-13.6 | None | None | +| TR-14.1 | None | None | +| TR-14.1.0 | None | None | +| TR-14.1.1 | None | None | +| TR-14.1.2 | None | None | +| TR-14.1.3 | None | None | +| TR-14.1.4 | None | None | +| TR-14.1.5 | None | None | +| TR-14.1.6 | None | None | +| TR-14.1.7 | None | None | +| TR-14.1.8 | None | None | +| TR-14.1.9 | None | None | +| TR-14.1.10 | None | None | +| TR-14.2 | None | None | +| TR-14.3 | None | None | | TR-14.4 | None | None | | TR-14.5 | None | None | -| TR-15.1 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-15.2 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`) | -| TR-15.3 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `AuthUserServiceTests.*` (`tests/RemoteAgent.Service.Tests/AuthUserServiceTests.cs`) | -| TR-15.4 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-15.5 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-15.1 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-15.2 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-15.3 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-15.4 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-15.5 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`) | | TR-15.6 | Covered | `ConnectionProtectionServiceTests.*` (`tests/RemoteAgent.Service.Tests/ConnectionProtectionServiceTests.cs`) | | TR-15.7 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | TR-15.8 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | TR-15.9 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-15.10 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`), `AuthUserServiceTests.*` (`tests/RemoteAgent.Service.Tests/AuthUserServiceTests.cs`) | -| TR-16.1 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-16.2 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-16.3 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-16.4 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-16.5 | Partial | `MainWindowUiTests.*` (`tests/RemoteAgent.Desktop.UiTests/MainWindowUiTests.cs`) | -| TR-16.6 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | -| TR-16.7 | Partial | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-15.10 | Covered | `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | +| TR-16.1 | None | None | +| TR-16.2 | None | None | +| TR-16.3 | None | None | +| TR-16.4 | None | None | +| TR-16.5 | None | None | +| TR-16.6 | None | None | +| TR-16.7 | None | None | | TR-17.1 | Covered | `PromptTemplateServiceTests.*` (`tests/RemoteAgent.Service.Tests/PromptTemplateServiceTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | | TR-17.2 | Covered | `PromptTemplateServiceTests.*` (`tests/RemoteAgent.Service.Tests/PromptTemplateServiceTests.cs`) | -| TR-17.3 | Covered | `PromptTemplateEngineTests.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests.cs`), `PromptTemplateEngineTests_EdgeCases.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests_EdgeCases.cs`) | -| TR-17.4 | Partial | `PromptTemplateEngineTests.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests.cs`), `AgentGatewayServiceIntegrationTests_ManagementApis.*` (`tests/RemoteAgent.Service.IntegrationTests/AgentGatewayServiceIntegrationTests_ManagementApis.cs`) | - -## Notes - -- This is an automated-test matrix only. Some `None` rows may still be validated by implementation review, workflow checks, or manual QA. -- If you want, this matrix can be tightened further by adding direct FR/TR annotations in every test method and generating this file automatically from source comments. +| TR-17.3 | Covered | `PromptTemplateEngineTests.*` (`tests/RemoteAgent.App.Tests/PromptTemplateEngineTests.cs`) | +| TR-17.4 | None | None | +| TR-18.1 | Covered | `RefreshOpenSessionsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshOpenSessionsHandlerTests.cs`), `BanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/BanPeerHandlerTests.cs`), `DeleteAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteAuthUserHandlerTests.cs`), `ClearAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ClearAppLogHandlerTests.cs`), `SendDesktopMessageHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SendDesktopMessageHandlerTests.cs`), `SaveAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAppLogHandlerTests.cs`), `ExpandStatusLogPanelHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ExpandStatusLogPanelHandlerTests.cs`), `RemoveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RemoveServerRegistrationHandlerTests.cs`), `LiteDbLocalStorageTests.*` (`tests/RemoteAgent.Service.Tests/LiteDbLocalStorageTests.cs`), `SetManagementSectionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetManagementSectionHandlerTests.cs`), `TerminateDesktopSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateDesktopSessionHandlerTests.cs`), `DeleteMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteMcpServerHandlerTests.cs`), `SavePluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePluginsHandlerTests.cs`), `UnbanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/UnbanPeerHandlerTests.cs`), `RefreshAuthUsersHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshAuthUsersHandlerTests.cs`), `SeedSessionContextHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SeedSessionContextHandlerTests.cs`), `DeletePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeletePromptTemplateHandlerTests.cs`), `OpenNewSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/OpenNewSessionHandlerTests.cs`), `StartLogMonitoringHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/StartLogMonitoringHandlerTests.cs`), `RefreshSecurityDataHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshSecurityDataHandlerTests.cs`), `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`), `SaveAgentMcpMappingHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAgentMcpMappingHandlerTests.cs`), `CheckSessionCapacityHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckSessionCapacityHandlerTests.cs`), `ServiceProviderRequestDispatcherTests.*` (`tests/RemoteAgent.App.Tests/Cqrs/ServiceProviderRequestDispatcherTests.cs`), `CheckLocalServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckLocalServerHandlerTests.cs`), `ApplyLocalServerActionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ApplyLocalServerActionHandlerTests.cs`), `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`), `SaveMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveMcpServerHandlerTests.cs`), `SaveAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAuthUserHandlerTests.cs`), `ConnectionSettingsDialogViewModelTests.*` (`tests/RemoteAgent.Desktop.UiTests/ViewModels/ConnectionSettingsDialogViewModelTests.cs`), `TerminateOpenServerSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateOpenServerSessionHandlerTests.cs`), `RefreshMcpRegistryHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshMcpRegistryHandlerTests.cs`), `RefreshPromptTemplatesHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPromptTemplatesHandlerTests.cs`), `SavePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePromptTemplateHandlerTests.cs`), `RefreshPluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPluginsHandlerTests.cs`) | +| TR-18.2 | Covered | `RefreshOpenSessionsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshOpenSessionsHandlerTests.cs`), `BanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/BanPeerHandlerTests.cs`), `DeleteAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteAuthUserHandlerTests.cs`), `ClearAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ClearAppLogHandlerTests.cs`), `SendDesktopMessageHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SendDesktopMessageHandlerTests.cs`), `SaveAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAppLogHandlerTests.cs`), `ExpandStatusLogPanelHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ExpandStatusLogPanelHandlerTests.cs`), `RemoveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RemoveServerRegistrationHandlerTests.cs`), `SetManagementSectionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetManagementSectionHandlerTests.cs`), `TerminateDesktopSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateDesktopSessionHandlerTests.cs`), `DeleteMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteMcpServerHandlerTests.cs`), `SavePluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePluginsHandlerTests.cs`), `UnbanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/UnbanPeerHandlerTests.cs`), `RefreshAuthUsersHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshAuthUsersHandlerTests.cs`), `SeedSessionContextHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SeedSessionContextHandlerTests.cs`), `DeletePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeletePromptTemplateHandlerTests.cs`), `OpenNewSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/OpenNewSessionHandlerTests.cs`), `StartLogMonitoringHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/StartLogMonitoringHandlerTests.cs`), `RefreshSecurityDataHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshSecurityDataHandlerTests.cs`), `StructuredLogServiceTests.*` (`tests/RemoteAgent.Service.Tests/StructuredLogServiceTests.cs`), `SaveAgentMcpMappingHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAgentMcpMappingHandlerTests.cs`), `CheckSessionCapacityHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckSessionCapacityHandlerTests.cs`), `ServiceProviderRequestDispatcherTests.*` (`tests/RemoteAgent.App.Tests/Cqrs/ServiceProviderRequestDispatcherTests.cs`), `CheckLocalServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckLocalServerHandlerTests.cs`), `ApplyLocalServerActionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ApplyLocalServerActionHandlerTests.cs`), `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`), `SaveMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveMcpServerHandlerTests.cs`), `SaveAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAuthUserHandlerTests.cs`), `ConnectionSettingsDialogViewModelTests.*` (`tests/RemoteAgent.Desktop.UiTests/ViewModels/ConnectionSettingsDialogViewModelTests.cs`), `TerminateOpenServerSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateOpenServerSessionHandlerTests.cs`), `RefreshMcpRegistryHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshMcpRegistryHandlerTests.cs`), `RefreshPromptTemplatesHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPromptTemplatesHandlerTests.cs`), `SavePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePromptTemplateHandlerTests.cs`), `RefreshPluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPluginsHandlerTests.cs`) | +| TR-18.3 | Covered | `RefreshOpenSessionsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshOpenSessionsHandlerTests.cs`), `BanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/BanPeerHandlerTests.cs`), `DeleteAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteAuthUserHandlerTests.cs`), `ClearAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ClearAppLogHandlerTests.cs`), `SendDesktopMessageHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SendDesktopMessageHandlerTests.cs`), `SaveAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAppLogHandlerTests.cs`), `ExpandStatusLogPanelHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ExpandStatusLogPanelHandlerTests.cs`), `RemoveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RemoveServerRegistrationHandlerTests.cs`), `SetManagementSectionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetManagementSectionHandlerTests.cs`), `TerminateDesktopSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateDesktopSessionHandlerTests.cs`), `DeleteMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteMcpServerHandlerTests.cs`), `SavePluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePluginsHandlerTests.cs`), `UnbanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/UnbanPeerHandlerTests.cs`), `RefreshAuthUsersHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshAuthUsersHandlerTests.cs`), `SeedSessionContextHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SeedSessionContextHandlerTests.cs`), `DeletePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeletePromptTemplateHandlerTests.cs`), `OpenNewSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/OpenNewSessionHandlerTests.cs`), `StartLogMonitoringHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/StartLogMonitoringHandlerTests.cs`), `RefreshSecurityDataHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshSecurityDataHandlerTests.cs`), `SaveAgentMcpMappingHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAgentMcpMappingHandlerTests.cs`), `CheckSessionCapacityHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckSessionCapacityHandlerTests.cs`), `ServiceProviderRequestDispatcherTests.*` (`tests/RemoteAgent.App.Tests/Cqrs/ServiceProviderRequestDispatcherTests.cs`), `CheckLocalServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckLocalServerHandlerTests.cs`), `ApplyLocalServerActionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ApplyLocalServerActionHandlerTests.cs`), `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`), `SaveMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveMcpServerHandlerTests.cs`), `SaveAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAuthUserHandlerTests.cs`), `ConnectionSettingsDialogViewModelTests.*` (`tests/RemoteAgent.Desktop.UiTests/ViewModels/ConnectionSettingsDialogViewModelTests.cs`), `TerminateOpenServerSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateOpenServerSessionHandlerTests.cs`), `RefreshMcpRegistryHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshMcpRegistryHandlerTests.cs`), `RefreshPromptTemplatesHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPromptTemplatesHandlerTests.cs`), `SavePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePromptTemplateHandlerTests.cs`), `RefreshPluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPluginsHandlerTests.cs`) | +| TR-18.4 | Covered | `RefreshOpenSessionsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshOpenSessionsHandlerTests.cs`), `BanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/BanPeerHandlerTests.cs`), `DeleteAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteAuthUserHandlerTests.cs`), `ClearAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ClearAppLogHandlerTests.cs`), `SendDesktopMessageHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SendDesktopMessageHandlerTests.cs`), `SaveAppLogHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAppLogHandlerTests.cs`), `ExpandStatusLogPanelHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ExpandStatusLogPanelHandlerTests.cs`), `RemoveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RemoveServerRegistrationHandlerTests.cs`), `SetManagementSectionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetManagementSectionHandlerTests.cs`), `TerminateDesktopSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateDesktopSessionHandlerTests.cs`), `DeleteMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeleteMcpServerHandlerTests.cs`), `SavePluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePluginsHandlerTests.cs`), `UnbanPeerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/UnbanPeerHandlerTests.cs`), `RefreshAuthUsersHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshAuthUsersHandlerTests.cs`), `SeedSessionContextHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SeedSessionContextHandlerTests.cs`), `DeletePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/DeletePromptTemplateHandlerTests.cs`), `OpenNewSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/OpenNewSessionHandlerTests.cs`), `StartLogMonitoringHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/StartLogMonitoringHandlerTests.cs`), `RefreshSecurityDataHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshSecurityDataHandlerTests.cs`), `SaveAgentMcpMappingHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAgentMcpMappingHandlerTests.cs`), `CheckSessionCapacityHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckSessionCapacityHandlerTests.cs`), `CheckLocalServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/CheckLocalServerHandlerTests.cs`), `ApplyLocalServerActionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/ApplyLocalServerActionHandlerTests.cs`), `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`), `SaveMcpServerHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveMcpServerHandlerTests.cs`), `SaveAuthUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveAuthUserHandlerTests.cs`), `ConnectionSettingsDialogViewModelTests.*` (`tests/RemoteAgent.Desktop.UiTests/ViewModels/ConnectionSettingsDialogViewModelTests.cs`), `TerminateOpenServerSessionHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/TerminateOpenServerSessionHandlerTests.cs`), `RefreshMcpRegistryHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshMcpRegistryHandlerTests.cs`), `RefreshPromptTemplatesHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPromptTemplatesHandlerTests.cs`), `SavePromptTemplateHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SavePromptTemplateHandlerTests.cs`), `RefreshPluginsHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/RefreshPluginsHandlerTests.cs`) | +| TR-19.1 | Partial | `SetPairingUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetPairingUserHandlerTests.cs`) | +| TR-19.2 | None | None | +| TR-19.3 | None | None | +| TR-19.4 | None | None | +| TR-19.5 | None | None | +| TR-19.6 | Partial | `ScanQrCodeHandlerTests.*` (`tests/RemoteAgent.App.Tests/ScanQrCodeHandlerTests.cs`) | +| TR-19.7 | Partial | `ScanQrCodeHandlerTests.*` (`tests/RemoteAgent.App.Tests/ScanQrCodeHandlerTests.cs`) | +| TR-19.8 | Partial | `SetPairingUserHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SetPairingUserHandlerTests.cs`) | +| TR-19.9 | Partial | `ScanQrCodeHandlerTests.*` (`tests/RemoteAgent.App.Tests/ScanQrCodeHandlerTests.cs`) | +| TR-20.1 | None | None | +| TR-20.2 | None | None | +| TR-21.1 | Partial | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | +| TR-21.2 | None | None | +| TR-21.3 | Covered | `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`) | +| TR-21.4 | Partial | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | +| TR-21.5 | None | None | +| TR-21.6 | Covered | `SaveServerRegistrationHandlerTests.*` (`tests/RemoteAgent.Desktop.UiTests/Handlers/SaveServerRegistrationHandlerTests.cs`) | +| TR-21.7 | None | None | +| TR-22.1 | None | None | +| TR-22.2 | Covered | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | +| TR-22.3 | None | None | +| TR-22.4 | None | None | +| TR-22.5 | Covered | `MobileHandlerTests.*` (`tests/RemoteAgent.App.Tests/MobileHandlerTests.cs`) | diff --git a/docs/technical-requirements.md b/docs/technical-requirements.md index a20b775..f877ae6 100644 --- a/docs/technical-requirements.md +++ b/docs/technical-requirements.md @@ -65,7 +65,7 @@ - **TR-5.4** When the app receives a message with **notify** priority, it shall **show a system notification** (e.g. on Android, using a notification channel and `NotificationManager`/`NotificationCompat`); tapping the notification shall open the app so the message is visible in the chat. - **TR-5.5** The app shall support **swipe gestures** (e.g. left or right) on a message to **archive** it; archived messages shall be hidden from the visible list (e.g. via a property on the message and binding or filtering). - **TR-5.6** The chat input control shall be multi-line (editor semantics), preserving newline characters in submitted requests. -- **TR-5.7** Desktop keyboard handling shall support **Ctrl+Enter** as the submit accelerator while plain Enter inserts a newline. +- **TR-5.7** Desktop keyboard handling shall support **Ctrl+Enter** as the submit accelerator while plain Enter inserts a newline. Mobile keyboard handling shall use **Enter** as the submit accelerator. - **TR-5.8** Mobile UX shall use a connection-first screen (host/port/session connect) and toggle to the chat workspace only after successful connection state. *See:* [FR-1](functional-requirements.md#1-product-purpose), [FR-2](functional-requirements.md#2-chat-and-messaging), [FR-3](functional-requirements.md#3-message-priority-and-notifications), [FR-4](functional-requirements.md#4-archive). @@ -236,3 +236,54 @@ - **TR-18.4** UI tests shall validate known use cases by substituting mocked CQRS handlers and asserting rendered UI state changes, status messages, and command completion/error behavior. *See:* [FR-12](functional-requirements.md#12-desktop-management-app), [FR-2](functional-requirements.md#2-chat-and-messaging), [TR-8](#8-testing), [TR-14](#14-desktop-management-capabilities). + +--- + +## 19. Device pairing and API key management + +- **TR-19.1** The service shall expose a `SetPairingUsers` gRPC RPC that accepts username/password pairs, **hashes passwords with SHA-256**, generates a **32-byte cryptographically random hex API key**, writes both credentials and key to `appsettings.json`, and returns the generated key in the response (`SetPairingUsersResponse.generated_api_key`). +- **TR-19.2** The `/pair` web endpoints shall use **`IOptionsMonitor.CurrentValue`** (not `IOptions`) so that configuration changes from `SetPairingUsers` are reflected immediately without service restart. +- **TR-19.3** The service shall listen on a **secondary HTTP/1+HTTP/2 web port** (computed as `"1" + gRPC port`, e.g. 15244) for browser-accessible endpoints including the `/pair` login and `/pair/key` pages. +- **TR-19.4** The `/pair/key` page shall render the API key, a QR code generated **server-side** via `QRCoder.PngByteQRCode` embedded as a `data:image/png;base64,…` `` tag (no external CDN or JavaScript dependency), and a deep-link anchor (``). +- **TR-19.5** The deep-link URI format shall be `remoteagent://pair?key={apiKey}&host={serverHost}&port={gRPCport}` where `port` is the **gRPC port** (not the web port) and all values are URI-escaped. +- **TR-19.6** The mobile app shall implement the Login flow via a modal **`PairLoginPage`** containing a `WebView` that loads `http://{host}:1{port}/pair`; on navigation to `/pair/key`, the page shall execute `document.querySelector('a.btn')?.getAttribute('href')` via `EvaluateJavaScriptAsync` to extract the deep-link URI, strip any JSON-quoting artifacts from Android's WebView, resolve a `TaskCompletionSource`, and dismiss the modal. +- **TR-19.7** The `ScanQrCodeHandler` shall validate that a host is configured before building the login URL, construct the web port as `"1" + gRPC port`, and parse the returned `remoteagent://` URI to populate `Host`, `Port`, and `ApiKey` on the view model. +- **TR-19.8** The desktop `SetPairingUsersAsync` client method shall return `Task` (the generated API key) rather than `Task`, and the handler shall apply the returned key to the workspace immediately. +- **TR-19.9** The `IQrCodeScanner` abstraction shall accept a `loginUrl` parameter (`Task ScanAsync(string loginUrl)`) to support the web-based login flow instead of camera-based QR scanning. + +*See:* [FR-17](functional-requirements.md#17-device-pairing-and-api-key-management). + +--- + +## 20. Desktop UX refinements + +- **TR-20.1** All `TextBlock` controls in Avalonia desktop `.axaml` view files shall be replaced with **`SelectableTextBlock`** to enable text selection and copy. +- **TR-20.2** A **global Avalonia style** in `App.axaml` shall apply a **4px margin** to `Button`, `TextBox`, `ComboBox`, `CheckBox`, `ListBox`, `SelectableTextBlock`, and `TabControl` controls. + +*See:* [FR-18](functional-requirements.md#18-desktop-ux-refinements). + +--- + +## 21. Server profiles and persistent connection settings + +- **TR-21.1** A shared `ServerProfile` model and `IServerProfileStore` interface shall be defined in `RemoteAgent.App.Logic` so both mobile and desktop can consume the same contract. +- **TR-21.2** The mobile app shall implement `IServerProfileStore` using **LiteDB** with a unique index on `host:port` (lowercased). CRUD operations: `GetAll`, `GetByHostPort`, `Upsert`, `Delete`. +- **TR-21.3** The desktop `ServerRegistration` model shall include `PerRequestContext` and `DefaultSessionContext` string properties, persisted by `LiteDbServerRegistrationStore.Upsert`. +- **TR-21.4** `ConnectMobileSessionHandler` shall **auto-save** a `ServerProfile` on successful connection and load the saved `PerRequestContext` into the workspace when the workspace value is empty. +- **TR-21.5** The desktop `ServerWorkspaceViewModel` shall load `PerRequestContext` from the `ServerRegistration` during construction. +- **TR-21.6** The desktop `SaveServerRegistrationRequest` and `SaveServerRegistrationHandler` shall accept and persist `PerRequestContext` and `DefaultSessionContext`. +- **TR-21.7** The mobile `SettingsPage` shall be backed by a `SettingsPageViewModel` (DI-registered) that exposes the profile list with save and delete commands. + +*See:* [FR-19](functional-requirements.md#19-server-profiles-and-persistent-connection-settings). + +--- + +## 22. Mobile chat UX + +- **TR-22.1** On Android, the native `AppCompatEditText` platform view of the message `Editor` shall intercept `Keycode.Enter` on `KeyEventActions.Down` and dispatch `SendMessageCommand`, preventing newline insertion. +- **TR-22.2** The `IConnectionModeSelector` interface and `MauiConnectionModeSelector` implementation shall be removed; `ConnectMobileSessionHandler` shall hardcode `ConnectionMode = "server"`. +- **TR-22.3** `ISessionCommandBus` shall expose `IsConnected` (bool) and `ConnectionStateChanged` (event) so `AppShellViewModel` can bind flyout item visibility to connection state. +- **TR-22.4** The connection card in `MainPage.xaml` shall bind `IsVisible` to `IsConnected` via `InverseBool` converter, hiding it once connected. +- **TR-22.5** `ServerApiClient` shall use `ExecuteGrpcAsync` for all 12 server API methods; no `HttpClient`-based REST calls shall remain. + +*See:* [FR-20](functional-requirements.md#20-mobile-chat-ux). diff --git a/docs/toc.yml b/docs/toc.yml index 8dd8148..c1678d8 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -17,6 +17,8 @@ href: REPOSITORY_RULES.md - name: Branch protection href: branch-protection.md + - name: FAQ + href: faq.md - name: Testing href: testing-strategy.md diff --git a/msix.yml b/msix.yml new file mode 100644 index 0000000..ed7c812 --- /dev/null +++ b/msix.yml @@ -0,0 +1,47 @@ +# msix.yml — MSIX package configuration for Remote Agent +# Used by the MsixTools PowerShell module (scripts/MsixTools). +# +# Usage: +# .\scripts\package-msix.ps1 [-Clean] [-DevCert] [-Force] [-Install] +# .\scripts\package-msix.ps1 -BumpPatch -Clean -Force -Install +# +# Any value here can be overridden by passing the corresponding parameter +# directly to package-msix.ps1 or New-MsixPackage. + +package: + name: RemoteAgent + displayName: Remote Agent + publisher: CN=RemoteAgent Dev + +service: + path: src/RemoteAgent.Service/RemoteAgent.Service.csproj + framework: net10.0 + serviceName: RemoteAgentService + displayName: Remote Agent Service + subDir: service + startAccount: localSystem + startupType: auto + +desktop: + path: src/RemoteAgent.Desktop/RemoteAgent.Desktop.csproj + framework: net9.0 + subDir: desktop + displayName: Remote Agent Desktop + appId: RemoteAgentApp + processName: RemoteAgent.Desktop # process name to kill before update + +plugins: + - path: src/RemoteAgent.Plugins.Ollama/RemoteAgent.Plugins.Ollama.csproj + framework: net10.0 + destSubDir: service/plugins + +build: + configuration: Release + rid: win-x64 + selfContained: true + +output: + dir: artifacts + +icons: + svg: src/RemoteAgent.Desktop/Assets/AppIcon/appicon.svg diff --git a/scripts/MsixTools b/scripts/MsixTools new file mode 160000 index 0000000..16ffb08 --- /dev/null +++ b/scripts/MsixTools @@ -0,0 +1 @@ +Subproject commit 16ffb08e461dd27341be909ef0dd9dfca2ed02e7 diff --git a/scripts/build-msix.ps1 b/scripts/build-msix.ps1 index 34702c7..1e01d05 100644 --- a/scripts/build-msix.ps1 +++ b/scripts/build-msix.ps1 @@ -1,100 +1,60 @@ #Requires -Version 7.0 <# .SYNOPSIS - Build the Remote Agent MSIX package and optionally install it. + Convenience script to build and optionally install the Remote Agent MSIX. .DESCRIPTION - Convenience wrapper around scripts/package-msix.ps1. - Publishes the service (net10.0) and desktop app (net9.0), packs a combined - MSIX, signs it with a self-signed dev certificate, and — when -Install is - specified — installs the package and starts the Windows service. - - Equivalent to: - .\scripts\package-msix.ps1 -Configuration -DevCert [-SelfContained] - .\scripts\install-remote-agent.ps1 -CertPath artifacts\remote-agent-dev.cer (if -Install) + Simplified wrapper around New-MsixPackage (scripts/MsixTools). + For full control use package-msix.ps1. .PARAMETER Configuration Build configuration: Release (default) or Debug. .PARAMETER SelfContained - Bundle the .NET runtime inside the package (no runtime prereq on the target - machine). Results in a larger MSIX. Default: false. + Bundle the .NET runtime. Default: true. .PARAMETER Install - After building, install the MSIX on this machine and start the service. - Requires the script to be run as Administrator. + Install the MSIX and start the service after packaging. Requires Administrator. .PARAMETER NoCert - Skip self-signed certificate creation and produce an unsigned package. - The package can still be installed locally with Add-AppxPackage -AllowUnsigned. + Skip signing (auto-signed on install by Install-MsixPackage). -.EXAMPLE - # Build a signed dev package (Release): - .\scripts\build-msix.ps1 +.PARAMETER NoBuild + Skip dotnet publish; repackage existing artifacts\publish-* output. - # Build Debug, bundle runtime: - .\scripts\build-msix.ps1 -Configuration Debug -SelfContained +.PARAMETER Force + Skip the AppxManifest review pause. - # Build and install on this machine: +.EXAMPLE + .\scripts\build-msix.ps1 .\scripts\build-msix.ps1 -Install - - # Build unsigned (no cert): - .\scripts\build-msix.ps1 -NoCert + .\scripts\build-msix.ps1 -NoBuild -Force #> [CmdletBinding()] param( - [ValidateSet("Debug", "Release")] - [string] $Configuration = "Release", - - [switch] $SelfContained, - + [ValidateSet('Debug', 'Release')] + [string] $Configuration = 'Release', + [bool] $SelfContained = $true, [switch] $Install, - - [switch] $NoCert + [switch] $NoCert, + [switch] $NoBuild, + [switch] $Force ) -$ErrorActionPreference = "Stop" -$ScriptDir = $PSScriptRoot -$RepoRoot = (Get-Item $ScriptDir).Parent.FullName -$ArtifactsDir = Join-Path $RepoRoot "artifacts" +$ErrorActionPreference = 'Stop' +$RepoRoot = (Get-Item $PSScriptRoot).Parent.FullName +Import-Module (Join-Path $PSScriptRoot 'MsixTools\MsixTools.psd1') -Force -# ── Validate ────────────────────────────────────────────────────────────────── -if ($Install -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( - [Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error "-Install requires the script to be run as Administrator. Re-run from an elevated prompt." -} - -# ── Build the MSIX ──────────────────────────────────────────────────────────── -Write-Host "[build-msix] configuration : $Configuration" -Write-Host "[build-msix] self-contained: $SelfContained" -Write-Host "[build-msix] sign (dev cert): $(-not $NoCert)" -Write-Host "" - -$packageParams = @{ +$params = @{ + WorkspaceRoot = $RepoRoot + ConfigPath = Join-Path $RepoRoot 'msix.yml' Configuration = $Configuration - OutDir = $ArtifactsDir -} -if ($SelfContained) { $packageParams["SelfContained"] = $true } -if (-not $NoCert) { $packageParams["DevCert"] = $true } - -& "$ScriptDir\package-msix.ps1" @packageParams -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -# ── Install (optional) ──────────────────────────────────────────────────────── -if ($Install) { - Write-Host "" - Write-Host "[build-msix] installing package and starting service..." - - $installParams = @{} - $cerPath = Join-Path $ArtifactsDir "remote-agent-dev.cer" - if (-not $NoCert -and (Test-Path $cerPath)) { - $installParams["CertPath"] = $cerPath - } - - & "$ScriptDir\install-remote-agent.ps1" @installParams - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + SelfContained = $SelfContained + NoBuild = $NoBuild + Force = $Force + Install = $Install } +if (-not $NoCert) { $params['DevCert'] = $true } -Write-Host "" -Write-Host "[build-msix] done." +New-MsixPackage @params diff --git a/scripts/bump-minor-version.sh b/scripts/bump-minor-version.sh new file mode 100644 index 0000000..a03ec3e --- /dev/null +++ b/scripts/bump-minor-version.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# bump-minor-version.sh +# Increments the minor version in GitVersion.yml and resets the patch to 0. +# Usage: ./scripts/bump-minor-version.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GV_FILE="$REPO_ROOT/GitVersion.yml" + +# Extract current next-version value (e.g. "0.1.0") +current=$(grep -E '^next-version:' "$GV_FILE" | sed 's/next-version:[[:space:]]*//') + +IFS='.' read -r major minor patch <<< "$current" + +if [[ -z "$major" || -z "$minor" || -z "$patch" ]]; then + echo "error: could not parse next-version '$current' from $GV_FILE" >&2 + exit 1 +fi + +new_version="$major.$((minor + 1)).0" + +sed -i "s/^next-version:.*/next-version: $new_version/" "$GV_FILE" + +echo "Bumped next-version: $current -> $new_version" diff --git a/scripts/bump-patch-version.sh b/scripts/bump-patch-version.sh new file mode 100644 index 0000000..f44aa7c --- /dev/null +++ b/scripts/bump-patch-version.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# bump-patch-version.sh +# Increments the patch version in GitVersion.yml. +# Usage: ./scripts/bump-patch-version.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GV_FILE="$REPO_ROOT/GitVersion.yml" + +# Extract current next-version value (e.g. "0.1.0") +current=$(grep -E '^next-version:' "$GV_FILE" | sed 's/next-version:[[:space:]]*//') + +IFS='.' read -r major minor patch <<< "$current" + +if [[ -z "$major" || -z "$minor" || -z "$patch" ]]; then + echo "error: could not parse next-version '$current' from $GV_FILE" >&2 + exit 1 +fi + +new_version="$major.$minor.$((patch + 1))" + +sed -i "s/^next-version:.*/next-version: $new_version/" "$GV_FILE" + +echo "Bumped next-version: $current -> $new_version" diff --git a/scripts/generate-requirements-matrix.sh b/scripts/generate-requirements-matrix.sh new file mode 100755 index 0000000..3b3a445 --- /dev/null +++ b/scripts/generate-requirements-matrix.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Script to generate requirements-test-coverage.md from test method annotations +# This script scans all test files for [Trait("Requirement", "FR-X.X")] and [Trait("Requirement", "TR-X.X")] +# annotations and generates a traceability matrix + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +TESTS_DIR="$REPO_ROOT/tests" +OUTPUT_FILE="$REPO_ROOT/docs/requirements-test-coverage.md" +FR_FILE="$REPO_ROOT/docs/functional-requirements.md" +TR_FILE="$REPO_ROOT/docs/technical-requirements.md" + +echo "Scanning test files for requirement annotations..." + +# Temporary file for storing requirement mappings +TEMP_FR_MAP=$(mktemp) +TEMP_TR_MAP=$(mktemp) +trap 'rm -f "$TEMP_FR_MAP" "$TEMP_TR_MAP"' EXIT + +# Extract all FR and TR IDs from requirements files +extract_requirement_ids() { + local req_file="$1" + local prefix="$2" + # Match patterns like "- **FR-1.1**" or "- **TR-1.1**" + grep -oP "(?<=\*\*${prefix}-)[0-9]+(\.[0-9]+)*(?=\*\*)" "$req_file" 2>/dev/null || true +} + +echo "Extracting requirement IDs from requirement documents..." +FR_IDS=$(extract_requirement_ids "$FR_FILE" "FR" | sort -V | uniq) +TR_IDS=$(extract_requirement_ids "$TR_FILE" "TR" | sort -V | uniq) + +# Function to scan test files and extract requirement mappings +scan_test_files() { + local req_prefix="$1" + local output_map="$2" + + # Find all test files and process them + local test_files=$(find "$TESTS_DIR" -name "*Tests*.cs" -type f) + for test_file in $test_files; do + local rel_path="${test_file#$REPO_ROOT/}" + + # Read file with awk to better handle state + awk -v prefix="$req_prefix" -v outfile="$output_map" -v filepath="$rel_path" ' + BEGIN { + in_class = 0 + is_test_method = 0 + } + + # Match class-level Trait annotations (before class declaration) + !in_class && /\[Trait\("Requirement", "[A-Z]+-[0-9.]+"\)/ { + req_id = "" + if (match($0, /"[A-Z]+-[0-9.]+"/)) { + req_id = substr($0, RSTART + 1, RLENGTH - 2) + } + if (index(req_id, prefix) == 1) { + class_reqs[++class_req_count] = req_id + } + } + + # Match class declaration + /public class [a-zA-Z0-9_]+/ { + if (match($0, /public class [a-zA-Z0-9_]+/)) { + class_name = substr($0, RSTART + 13, RLENGTH - 13) + in_class = 1 + } + } + + # Match method-level Trait annotations (inside class) + in_class && /\[Trait\("Requirement", "[A-Z]+-[0-9.]+"\)/ { + req_id = "" + if (match($0, /"[A-Z]+-[0-9.]+"/)) { + req_id = substr($0, RSTART + 1, RLENGTH - 2) + } + if (index(req_id, prefix) == 1) { + method_reqs[++method_req_count] = req_id + } + } + + # Match test attributes + in_class && /\[(Fact|Theory)\]/ { + is_test_method = 1 + } + + # Match method declaration + in_class && /public (async )?(Task|void) [a-zA-Z0-9_]+\(/ { + if (match($0, /public (async )?(Task|void) [a-zA-Z0-9_]+\(/)) { + method_name = substr($0, RSTART, RLENGTH) + sub(/^public (async )?(Task|void) /, "", method_name) + sub(/\($/, "", method_name) + } else { + method_name = "" + } + + if (is_test_method) { + # Use method-level requirements if available, otherwise class-level + if (method_req_count > 0) { + for (i = 1; i <= method_req_count; i++) { + print method_reqs[i] "|" class_name "." method_name "|" filepath "|method" >> outfile + } + } else if (class_req_count > 0) { + for (i = 1; i <= class_req_count; i++) { + print class_reqs[i] "|" class_name "." method_name "|" filepath "|class" >> outfile + } + } + } + + # Reset method-level state + method_req_count = 0 + delete method_reqs + is_test_method = 0 + } + ' "$test_file" + done +} + +echo "Scanning for FR requirements..." +scan_test_files "FR" "$TEMP_FR_MAP" + +echo "Scanning for TR requirements..." +scan_test_files "TR" "$TEMP_TR_MAP" + +# Function to generate coverage row for a requirement +generate_coverage_row() { + local req_id="$1" + local map_file="$2" + + # Find all tests covering this requirement + local tests=$(grep "^${req_id}|" "$map_file" 2>/dev/null | sort -u || true) + + if [[ -z "$tests" ]]; then + echo "| $req_id | None | None |" + else + # Determine coverage level + local coverage="Covered" + + # Group by class to determine if we should use ClassName.* notation + declare -A class_methods + declare -A class_files + declare -A method_specific + + while IFS='|' read -r req method file source_type; do + [[ -z "$method" ]] && continue + local class="${method%%.*}" + local method_name="${method#*.}" + + if [[ "$source_type" == "method" ]]; then + # Method has specific requirements + method_specific["$class|$method"]="$file" + else + # Method inherits class requirements + if [[ -z "${class_methods[$class]:-}" ]]; then + class_methods["$class"]="$method_name" + else + class_methods["$class"]="${class_methods[$class]} $method_name" + fi + class_files["$class"]="$file" + fi + done <<< "$tests" + + # Format tests list + local tests_formatted="" + + # First, output classes with all methods (using .*) + for class in "${!class_methods[@]}"; do + if [[ -n "$tests_formatted" ]]; then + tests_formatted="${tests_formatted}, " + fi + tests_formatted="${tests_formatted}\`${class}.*\` (\`${class_files[$class]}\`)" + done + + # Then, output individual methods with specific requirements + for key in "${!method_specific[@]}"; do + local method="${key#*|}" + local file="${method_specific[$key]}" + if [[ -n "$tests_formatted" ]]; then + tests_formatted="${tests_formatted}, " + fi + tests_formatted="${tests_formatted}\`${method}\` (\`${file}\`)" + done + + echo "| $req_id | $coverage | $tests_formatted |" + fi +} + +echo "Generating requirements-test-coverage.md..." + +# Generate the output file +{ + echo "# Requirements Test Coverage" + echo "" + echo "This document maps every requirement ID in \`docs/functional-requirements.md\` (FR) and \`docs/technical-requirements.md\` (TR) to current automated test coverage." + echo "" + echo "**Note:** This file is auto-generated by \`scripts/generate-requirements-matrix.sh\`. Do not edit manually." + echo "" + echo "Coverage status values:" + echo "- \`Covered\`: direct automated test coverage exists." + echo "- \`Partial\`: some behavior is covered, but not the full requirement scope." + echo "- \`None\`: no automated test currently covers the requirement." + echo "" + echo "## Functional Requirements (FR)" + echo "" + echo "| Requirement | Coverage | Tests |" + echo "|---|---|---|" + + # Generate FR rows + while IFS= read -r fr_id; do + if [[ -n "$fr_id" ]]; then + generate_coverage_row "FR-${fr_id}" "$TEMP_FR_MAP" + fi + done <<< "$FR_IDS" + + echo "" + echo "## Technical Requirements (TR)" + echo "" + echo "| Requirement | Coverage | Tests |" + echo "|---|---|---|" + + # Generate TR rows + while IFS= read -r tr_id; do + if [[ -n "$tr_id" ]]; then + generate_coverage_row "TR-${tr_id}" "$TEMP_TR_MAP" + fi + done <<< "$TR_IDS" + +} > "$OUTPUT_FILE" + +echo "✅ Generated $OUTPUT_FILE" +echo "Total FR IDs: $(echo "$FR_IDS" | grep -c . || echo 0)" +echo "Total TR IDs: $(echo "$TR_IDS" | grep -c . || echo 0)" +echo "FR mappings: $(wc -l < "$TEMP_FR_MAP")" +echo "TR mappings: $(wc -l < "$TEMP_TR_MAP")" diff --git a/scripts/install-remote-agent.ps1 b/scripts/install-remote-agent.ps1 index dea5da3..001d6ef 100644 --- a/scripts/install-remote-agent.ps1 +++ b/scripts/install-remote-agent.ps1 @@ -2,46 +2,43 @@ #Requires -RunAsAdministrator <# .SYNOPSIS - Install the Remote Agent MSIX package and immediately start the Windows service. + Install (or uninstall) the Remote Agent MSIX package and manage the Windows service. .DESCRIPTION - 1. Optionally trusts the signing certificate (required for self-signed dev builds). - 2. Installs the MSIX package via Add-AppxPackage (or updates an existing install). - 3. Polls the Windows SCM until RemoteAgentService is registered (up to -TimeoutSeconds). - 4. Starts the service and reports its final status. + Wrapper around the MsixTools module's Install-MsixPackage / Uninstall-MsixPackage functions. + Package, service, and path configuration is read from msix.yml in the repository root. - The service is registered by the MSIX windows.service extension with StartupType=auto, - so it will also start automatically on every subsequent boot. - - Run as Administrator — Add-AppxPackage with a service extension requires elevation. + Install flow: + 1. Optionally trust the signing certificate (-CertPath). + 2. Auto-sign unsigned packages with a self-signed dev cert if needed. + 3. Stop RemoteAgentService and close RemoteAgent.Desktop before updating. + 4. Remove any existing version, then fresh-install (avoids HRESULT 0x80073CFB). + 5. Wait for the service to register in the SCM and start it. .PARAMETER MsixPath - Path to the .msix file. Defaults to the most recent remote-agent_*.msix in - \artifacts\. + Path to the .msix file. Defaults to the most recent remote-agent_*.msix in artifacts\. .PARAMETER CertPath - Path to a .cer file to trust before installing (required for self-signed dev packages). - Imports the certificate into Cert:\LocalMachine\Root. + Path to a .cer file to trust in Cert:\LocalMachine\Root before installing. .PARAMETER TimeoutSeconds - Seconds to wait for the service to appear in the SCM after package install. Default: 30. + Seconds to wait for RemoteAgentService to register in the SCM after install. Default: 30. .PARAMETER Uninstall - Remove the installed package and stop the service instead of installing. + Stop the service and remove the installed package instead of installing. .EXAMPLE - # Build then install (dev machine): - .\scripts\package-msix.ps1 -DevCert - .\scripts\install-remote-agent.ps1 -CertPath artifacts\remote-agent-dev.cer + # Build then install: + .\scripts\package-msix.ps1 -DevCert -Force -Install - # Install a release-signed package: - .\scripts\install-remote-agent.ps1 -MsixPath artifacts\remote-agent_2.1.0_x64.msix + # Install a specific package: + .\scripts\install-remote-agent.ps1 -MsixPath artifacts\remote-agent_0.1.0_x64.msix # Uninstall: .\scripts\install-remote-agent.ps1 -Uninstall #> -[CmdletBinding(SupportsShouldProcess)] +[CmdletBinding()] param( [string] $MsixPath = "", [string] $CertPath = "", @@ -50,119 +47,21 @@ param( ) $ErrorActionPreference = "Stop" -$RepoRoot = (Get-Item $PSScriptRoot).Parent.FullName -$ArtifactsDir = Join-Path $RepoRoot "artifacts" -$ServiceName = "RemoteAgentService" -$PackageFamilyPattern = "RemoteAgent*" +$RepoRoot = (Get-Item $PSScriptRoot).Parent.FullName +Import-Module (Join-Path $PSScriptRoot "MsixTools\MsixTools.psd1") -Force -# ── Uninstall path ──────────────────────────────────────────────────────────── -if ($Uninstall) { - Write-Host "[install] Stopping service '$ServiceName'..." - $svc = Get-Service $ServiceName -ErrorAction SilentlyContinue - if ($svc -and $svc.Status -ne "Stopped") { - Stop-Service $ServiceName -Force - Write-Host "[install] Service stopped." - } else { - Write-Host "[install] Service not running or not found." - } +$configPath = Join-Path $RepoRoot "msix.yml" - Write-Host "[install] Removing MSIX package..." - $pkg = Get-AppxPackage -Name "RemoteAgent" -ErrorAction SilentlyContinue - if ($pkg) { - Remove-AppxPackage -Package $pkg.PackageFullName - Write-Host "[install] Package removed: $($pkg.PackageFullName)" - } else { - Write-Host "[install] No installed RemoteAgent package found." - } +if ($Uninstall) { + Uninstall-MsixPackage -ConfigPath $configPath exit 0 } -# ── Resolve MSIX path ───────────────────────────────────────────────────────── -if (-not $MsixPath) { - $candidates = Get-ChildItem $ArtifactsDir -Filter "remote-agent_*.msix" -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime -Descending - if (-not $candidates) { - Write-Error "No remote-agent_*.msix found in $ArtifactsDir. Run scripts\package-msix.ps1 first." - } - $MsixPath = $candidates[0].FullName - Write-Host "[install] Using most recent package: $MsixPath" -} - -if (-not (Test-Path $MsixPath)) { - Write-Error "MSIX file not found: $MsixPath" -} - -# ── Trust signing certificate ───────────────────────────────────────────────── -if ($CertPath) { - if (-not (Test-Path $CertPath)) { - Write-Error "Certificate file not found: $CertPath" - } - Write-Host "[install] Importing certificate into Cert:\LocalMachine\Root..." - Import-Certificate -FilePath $CertPath -CertStoreLocation Cert:\LocalMachine\Root | Out-Null - Write-Host "[install] Certificate trusted." -} - -# ── Install or update the MSIX ──────────────────────────────────────────────── -$existing = Get-AppxPackage -Name "RemoteAgent" -ErrorAction SilentlyContinue -if ($existing) { - Write-Host "[install] Updating existing package ($($existing.Version) -> installing)..." - Add-AppxPackage -Path $MsixPath -ForceUpdateFromAnyVersion -} else { - Write-Host "[install] Installing package..." - Add-AppxPackage -Path $MsixPath -} -Write-Host "[install] Package installed." - -# ── Wait for the service to be registered in the SCM ───────────────────────── -# The MSIX windows.service extension registers the service asynchronously after -# the package install completes; poll until it appears or the timeout expires. -Write-Host "[install] Waiting for '$ServiceName' to be registered in the SCM..." -$deadline = (Get-Date).AddSeconds($TimeoutSeconds) -$svc = $null -while ((Get-Date) -lt $deadline) { - $svc = Get-Service $ServiceName -ErrorAction SilentlyContinue - if ($svc) { break } - Start-Sleep -Milliseconds 500 -} - -if (-not $svc) { - Write-Error @" -Timed out after ${TimeoutSeconds}s waiting for '$ServiceName' to appear in the SCM. -The MSIX package installed successfully but the service extension may not have registered yet. -Try starting manually: Start-Service $ServiceName -"@ -} - -Write-Host "[install] Service registered (status: $($svc.Status))." - -# ── Start the service ───────────────────────────────────────────────────────── -if ($svc.Status -eq "Running") { - Write-Host "[install] Service is already running." -} else { - Write-Host "[install] Starting '$ServiceName'..." - Start-Service $ServiceName - # Give it a moment and re-query. - Start-Sleep -Seconds 2 - $svc = Get-Service $ServiceName - Write-Host "[install] Service status: $($svc.Status)" - if ($svc.Status -ne "Running") { - Write-Warning "Service did not reach Running state. Check Event Viewer for details." - } +$params = @{ + ConfigPath = $configPath + TimeoutSeconds = $TimeoutSeconds } +if ($MsixPath) { $params['MsixPath'] = $MsixPath } +if ($CertPath) { $params['CertPath'] = $CertPath } -# ── Summary ─────────────────────────────────────────────────────────────────── -$pkg = Get-AppxPackage -Name "RemoteAgent" -Write-Host "" -Write-Host "── Remote Agent installed ───────────────────────────────────────────────" -Write-Host " Package : $($pkg.PackageFullName)" -Write-Host " Version : $($pkg.Version)" -Write-Host " Service : $ServiceName [$($svc.Status)] (StartType: Automatic)" -Write-Host "" -Write-Host " Service management:" -Write-Host " Stop-Service $ServiceName" -Write-Host " Start-Service $ServiceName" -Write-Host " Restart-Service $ServiceName" -Write-Host "" -Write-Host " Uninstall:" -Write-Host " .\scripts\install-remote-agent.ps1 -Uninstall" -Write-Host "────────────────────────────────────────────────────────────────────────" +Install-MsixPackage @params diff --git a/scripts/monitor-service-windows.ps1 b/scripts/monitor-service-windows.ps1 new file mode 100644 index 0000000..cc000d4 --- /dev/null +++ b/scripts/monitor-service-windows.ps1 @@ -0,0 +1,267 @@ +#Requires -Version 7.0 +<# +.SYNOPSIS + Monitor the Remote Agent Windows Service — Event Log errors and optional file log tail. + +.DESCRIPTION + Watches the Windows Application Event Log for entries from the Remote Agent Service + source and streams new errors/warnings to the console. Optionally also tails the + structured file log written to Agent.LogDirectory. + + Event IDs written by the service: + 1000 — service started successfully (Information) + 1001 — startup or runtime fatal error (Error) + 1002 — unhandled background-thread exception (Error) + + Run without -Tail to print recent history only, then exit. + Run with -Tail to stream new events continuously until Ctrl+C. + +.PARAMETER Tail + Stream new events continuously (poll every -IntervalSeconds). Default: $false. + +.PARAMETER IntervalSeconds + Polling interval when -Tail is active. Default: 5. + +.PARAMETER Hours + How many hours of history to show on startup. Default: 24. + +.PARAMETER Level + Minimum severity to display: Error | Warning | Information. Default: Warning. + +.PARAMETER LogFile + Path to the service file log (Agent.LogDirectory\service.log). + When set, the file log is tailed in parallel with the Event Log. + If omitted the script auto-detects the path from the service registry. + +.PARAMETER ServiceName + Windows Service name. Default: "Remote Agent Service". + +.EXAMPLE + .\scripts\monitor-service-windows.ps1 + .\scripts\monitor-service-windows.ps1 -Tail + .\scripts\monitor-service-windows.ps1 -Tail -Level Error -Hours 1 + .\scripts\monitor-service-windows.ps1 -Tail -LogFile "C:\ProgramData\RemoteAgent\logs\service.log" +#> +[CmdletBinding()] +param( + [switch] $Tail, + [int] $IntervalSeconds = 5, + [int] $Hours = 24, + [ValidateSet('Error','Warning','Information')] + [string] $Level = 'Warning', + [string] $LogFile = '', + [string] $ServiceName = 'Remote Agent Service' +) + +$ErrorActionPreference = 'Stop' + +# ── Severity filter ─────────────────────────────────────────────────────────── +# Get-WinEvent Level values: 1=Critical, 2=Error, 3=Warning, 4=Information, 5=Verbose +$levelOrder = @{ Information = 0; Warning = 1; Error = 2 } +$minLevel = $levelOrder[$Level] + +# Map WinEvent Level numbers to display names and severity order. +$winEventLevels = @{ + 1 = @{ Name = 'Critical'; Order = 2 } + 2 = @{ Name = 'Error'; Order = 2 } + 3 = @{ Name = 'Warning'; Order = 1 } + 4 = @{ Name = 'Information'; Order = 0 } + 5 = @{ Name = 'Verbose'; Order = 0 } +} + +function Get-EntryOrder([int]$level) { + $info = $winEventLevels[$level] + if ($null -eq $info) { return 0 } + return $info.Order +} + +function Get-EntryName([int]$level) { + $info = $winEventLevels[$level] + if ($null -eq $info) { return 'Unknown' } + return $info.Name +} + +function Test-SeverityIncluded([int]$level) { + return (Get-EntryOrder $level) -ge $minLevel +} + +# ── Colour map ──────────────────────────────────────────────────────────────── +$colours = @{ + Critical = 'Magenta' + Error = 'Red' + Warning = 'Yellow' + Information = 'Cyan' + Verbose = 'DarkGray' +} + +function Write-EventEntry($entry) { + $name = Get-EntryName $entry.Level + $colour = $colours[$name] ?? 'Gray' + $ts = $entry.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') + Write-Host "[$ts] [$name] EventId=$($entry.Id)" -ForegroundColor $colour + Write-Host $entry.Message -ForegroundColor $colour + Write-Host '' +} + +# ── Service status ──────────────────────────────────────────────────────────── +Write-Host "── Remote Agent Service Monitor ────────────────────────────────" -ForegroundColor Cyan + +$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($null -eq $svc) { + # Try a partial-name search in case the service was registered under a slightly different name. + $svc = Get-Service -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*Remote Agent*" -or $_.Name -like "*RemoteAgent*" } | + Select-Object -First 1 + if ($svc) { + Write-Host "Note: service '$ServiceName' not found; using '$($svc.Name)' instead." -ForegroundColor DarkYellow + $ServiceName = $svc.Name + } +} + +if ($null -eq $svc) { + Write-Warning "Windows service '$ServiceName' is not installed." +} else { + $statusColour = if ($svc.Status -eq 'Running') { 'Green' } else { 'Red' } + Write-Host "Service status : " -NoNewline + Write-Host $svc.Status -ForegroundColor $statusColour + Write-Host "Display name : $($svc.DisplayName)" +} +Write-Host '' + +# ── Auto-detect log file from service image path ────────────────────────────── +if (-not $LogFile -and $svc) { + try { + $imagePath = (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName" ` + -Name ImagePath -ErrorAction SilentlyContinue).ImagePath -replace '"','' + if ($imagePath) { + $installDir = Split-Path $imagePath -Parent + # Service defaults: Agent.LogDirectory resolves to %ProgramData%\RemoteAgent\logs or + # the directory next to the binary. Check both common locations. + $candidates = @( + "$env:ProgramData\RemoteAgent\logs\service.log", + (Join-Path $installDir 'logs\service.log'), + (Join-Path $installDir 'service.log') + ) + foreach ($c in $candidates) { + if (Test-Path $c) { $LogFile = $c; break } + } + } + } catch { } +} + +if ($LogFile) { + Write-Host "File log : $LogFile" -ForegroundColor DarkCyan +} else { + Write-Host "File log : (not found — Event Log only)" -ForegroundColor DarkGray +} +Write-Host '' + +# ── Shared helper: query Application Event Log via Get-WinEvent ─────────────── +function Get-ServiceEvents([datetime]$after, [datetime]$before = [datetime]::MaxValue) { + $filter = @{ + LogName = 'Application' + ProviderName = 'Remote Agent Service' + StartTime = $after + } + if ($before -ne [datetime]::MaxValue) { $filter['EndTime'] = $before } + try { + $events = Get-WinEvent -FilterHashtable $filter -ErrorAction Stop | + Where-Object { Test-SeverityIncluded $_.Level } | + Sort-Object TimeCreated + return $events + } catch [System.Exception] { + if ($_.Exception.Message -like '*No events were found*' -or + $_.Exception.HResult -eq -2147024809) { + return @() # no matching events — normal condition + } + throw + } +} + +# ── Event Log: recent history ───────────────────────────────────────────────── +$since = (Get-Date).AddHours(-$Hours) +$eventLog = 'Application' +$source = $ServiceName + +Write-Host "── Event Log history (last $Hours h, level >= $Level) ──────────────" -ForegroundColor Cyan + +try { + $entries = Get-ServiceEvents -after $since + if ($entries.Count -eq 0) { + Write-Host ' (no matching events)' -ForegroundColor DarkGray + } else { + foreach ($e in $entries) { Write-EventEntry $e } + } +} catch { + Write-Warning "Could not read Event Log: $_" +} + +Write-Host '' + +if (-not $Tail) { + Write-Host "Tip: run with -Tail to stream new events continuously." -ForegroundColor DarkGray + exit 0 +} + +# ── Continuous tail ─────────────────────────────────────────────────────────── +Write-Host "── Streaming new events (Ctrl+C to stop) ───────────────────────" -ForegroundColor Cyan +Write-Host '' + +# Track the timestamp of the last event already shown. +$lastEventTime = [datetime]::UtcNow + +# Track file log position. +$fileLogStream = $null +$fileLogReader = $null +if ($LogFile -and (Test-Path $LogFile)) { + $fileLogStream = [System.IO.File]::Open($LogFile, + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::Read, + [System.IO.FileShare]::ReadWrite) + $fileLogStream.Seek(0, [System.IO.SeekOrigin]::End) | Out-Null + $fileLogReader = [System.IO.StreamReader]::new($fileLogStream, [System.Text.Encoding]::UTF8, $true) +} + +# Error keywords to highlight in the file log. +$errorPattern = [regex]'(?i)fail|error|exception|crit|unhandled|warn' + +try { + while ($true) { + # ── Poll Event Log ──────────────────────────────────────────────────── + try { + $newEntries = Get-ServiceEvents -after $lastEventTime + foreach ($e in $newEntries) { + Write-EventEntry $e + if ($e.TimeCreated -gt $lastEventTime) { + $lastEventTime = $e.TimeCreated + } + } + } catch { } + + # ── Poll file log ───────────────────────────────────────────────────── + if ($fileLogReader) { + while (-not $fileLogReader.EndOfStream) { + $line = $fileLogReader.ReadLine() + if ($null -eq $line) { break } + if ($errorPattern.IsMatch($line)) { + $lineColour = if ($line -match '(?i)fail|error|exception|crit|unhandled') { 'Red' } + elseif ($line -match '(?i)warn') { 'Yellow' } + else { 'Gray' } + Write-Host "[filelog] $line" -ForegroundColor $lineColour + } + } + } + + # ── Check service is still running ──────────────────────────────────── + $svcNow = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($svcNow -and $svcNow.Status -ne 'Running') { + Write-Host "[$(Get-Date -Format 'HH:mm:ss')] WARNING: service status changed to $($svcNow.Status)" ` + -ForegroundColor Red + } + + Start-Sleep -Seconds $IntervalSeconds + } +} finally { + $fileLogReader?.Dispose() + $fileLogStream?.Dispose() +} diff --git a/scripts/package-deb.sh b/scripts/package-deb.sh index 7ba9878..dcbeb0e 100644 --- a/scripts/package-deb.sh +++ b/scripts/package-deb.sh @@ -33,6 +33,7 @@ # /var/lib/remote-agent/ runtime data (LiteDB, media uploads) # /var/log/remote-agent/ session log files # /lib/systemd/system/ remote-agent.service unit +# /usr/sbin/remote-agent-ctl service management script (start/stop/restart/status) # # Desktop install layout: # /usr/lib/remote-agent/desktop/ application binaries @@ -196,6 +197,11 @@ if [[ "$BUILD_SERVICE" == "true" ]]; then cp -a "$SERVICE_PUBLISH/." "$SVC_DIR/usr/lib/remote-agent/service/" chmod 755 "$SVC_DIR/usr/lib/remote-agent/service/RemoteAgent.Service" + # ── Management script ───────────────────────────────────────────────────── + install -d "$SVC_DIR/usr/sbin" + install -m 755 "$SCRIPT_DIR/remote-agent-ctl.sh" \ + "$SVC_DIR/usr/sbin/remote-agent-ctl" + # ── Ollama plugin ───────────────────────────────────────────────────────── install -d "$SVC_DIR/usr/lib/remote-agent/plugins" cp "$PLUGIN_PUBLISH/RemoteAgent.Plugins.Ollama.dll" \ diff --git a/scripts/package-msix.ps1 b/scripts/package-msix.ps1 index 9cf861c..ba0c6f6 100644 --- a/scripts/package-msix.ps1 +++ b/scripts/package-msix.ps1 @@ -4,485 +4,112 @@ Build a combined MSIX package for the Remote Agent service and desktop management app. .DESCRIPTION - Publishes the service (net10.0, win-x64) and desktop app (net9.0, win-x64), assembles - them into a single MSIX package, and optionally signs it. - - The resulting package installs both: - - RemoteAgent.Service.exe, registered as a Windows service (RemoteAgentService) that - starts automatically under LocalSystem via the MSIX windows.service extension. - - RemoteAgent.Desktop.exe, visible in the Start menu as "Remote Agent Desktop". - - The Ollama plugin DLL is included in the service sub-directory when it has been built. - - Install layout inside the package: - service\ service binaries + appsettings.json - service\plugins\ Ollama plugin DLL (if built) - desktop\ desktop app binaries - Assets\ MSIX icon assets - - Requirements: - - Windows SDK >= 10.0.19041 (makeappx.exe, signtool.exe) - - .NET SDK 10.x (service + plugin) and 9.x (desktop; via src/RemoteAgent.Desktop/global.json) - - Signing: - -DevCert Create a temporary self-signed certificate (development / CI testing). - -CertThumbprint Use an existing certificate from the current user's My store. - Neither flag Build an unsigned package (can be installed with Add-AppxPackage -AllowUnsigned). - - After building, install on the same machine: - Add-AppxPackage -Path .\artifacts\remote-agent__x64.msix - - To trust a self-signed dev cert on the target machine before installing: - $cert = (Get-PfxCertificate -FilePath artifacts\remote-agent-dev.cer) - Import-Certificate -Certificate $cert -CertStoreLocation Cert:\LocalMachine\Root + Wrapper around New-MsixPackage (scripts/MsixTools). Reads all project and build + configuration from msix.yml in the repository root. .PARAMETER Configuration Build configuration: Release (default) or Debug. .PARAMETER Version - Package version (e.g. 1.2.3). Defaults to GitVersion SemVer, then most recent git tag, else 1.0.0. + Package version (e.g. 1.2.3). Auto-detected if omitted. .PARAMETER Publisher - MSIX Identity Publisher string, must match the signing certificate Subject exactly. - Default: "CN=RemoteAgent Dev". + MSIX Identity Publisher string. Default: read from msix.yml. .PARAMETER CertThumbprint - SHA1 thumbprint of an existing certificate in Cert:\CurrentUser\My to sign with. - Mutually exclusive with -DevCert. + SHA1 thumbprint of a certificate in Cert:\CurrentUser\My. Mutually exclusive with -DevCert. .PARAMETER DevCert - Create (or reuse) a self-signed certificate and sign the package with it. - The public certificate is exported to \remote-agent-dev.cer. + Create/reuse a self-signed dev certificate. Mutually exclusive with -CertThumbprint. .PARAMETER SelfContained - Publish self-contained packages (bundles .NET runtime; no runtime prereq on target). + Publish self-contained single-file. Default: true. .PARAMETER ServiceOnly - Build and bundle only the service component; omit the desktop app. + Omit the desktop app from the package. .PARAMETER DesktopOnly - Build and bundle only the desktop component; omit the service and Windows service extension. + Omit the service and windows.service extension from the package. -.PARAMETER OutDir - Output directory for the .msix file. Default: \artifacts\. +.PARAMETER Clean + Delete bin/ and obj/ before publishing. Mutually exclusive with -NoBuild. -.EXAMPLE - .\scripts\package-msix.ps1 -DevCert - .\scripts\package-msix.ps1 -Configuration Release -DevCert -SelfContained - .\scripts\package-msix.ps1 -CertThumbprint "ABC123DEF456..." -Version 2.1.0 -#> +.PARAMETER NoBuild + Skip dotnet publish; repackage existing artifacts\publish-* output. Mutually exclusive with -Clean. -[CmdletBinding()] -param( - [ValidateSet("Debug", "Release")] - [string] $Configuration = "Release", +.PARAMETER BumpMajor + Increment major version in GitVersion.yml before building. Mutually exclusive with -BumpMinor/-BumpPatch. - [string] $Version = "", +.PARAMETER BumpMinor + Increment minor version in GitVersion.yml before building. Mutually exclusive with -BumpMajor/-BumpPatch. - [string] $Publisher = "CN=RemoteAgent Dev", +.PARAMETER BumpPatch + Increment patch version in GitVersion.yml before building. Mutually exclusive with -BumpMajor/-BumpMinor. - [string] $CertThumbprint = "", +.PARAMETER Install + Install the MSIX and start the service after packaging. Requires Administrator. - [switch] $DevCert, +.PARAMETER Force + Skip the AppxManifest review pause. - [switch] $SelfContained, +.PARAMETER OutDir + Output directory. Default: \artifacts. - [switch] $ServiceOnly, +.EXAMPLE + .\scripts\package-msix.ps1 -DevCert -Force -Install + .\scripts\package-msix.ps1 -BumpPatch -Clean -Force -Install + .\scripts\package-msix.ps1 -NoBuild -Force +#> +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release')] + [string] $Configuration = 'Release', + [string] $Version = '', + [string] $Publisher = '', + [string] $CertThumbprint = '', + [switch] $DevCert, + [bool] $SelfContained = $true, + [switch] $ServiceOnly, [switch] $DesktopOnly, - - [string] $OutDir = "" -) - -$ErrorActionPreference = "Stop" -$RepoRoot = (Get-Item $PSScriptRoot).Parent.FullName -$NuGetConfig = Join-Path $RepoRoot "NuGet.Config" -if (-not $OutDir) { $OutDir = Join-Path $RepoRoot "artifacts" } - -# ── Validate mutually exclusive flags ──────────────────────────────────────── -if ($ServiceOnly -and $DesktopOnly) { - Write-Error "-ServiceOnly and -DesktopOnly are mutually exclusive." -} -if ($DevCert -and $CertThumbprint) { - Write-Error "-DevCert and -CertThumbprint are mutually exclusive." -} - -$BuildService = -not $DesktopOnly -$BuildDesktop = -not $ServiceOnly - -# ── Version detection ───────────────────────────────────────────────────────── -if (-not $Version) { - # Prefer GitVersion (dotnet tool) for accurate semver from branch/tag history. - try { - $gvJson = dotnet tool run dotnet-gitversion -- /output json 2>$null | ConvertFrom-Json - if ($gvJson -and $gvJson.SemVer) { $Version = $gvJson.SemVer } - } catch { } - - if (-not $Version) { - # Fallback: most recent git tag. - $tag = git -C $RepoRoot describe --tags --abbrev=0 2>$null - if ($tag -match '^v?(\d+\.\d+\.\d+)') { $Version = $Matches[1] } else { $Version = "1.0.0" } - } -} - -# MSIX Identity Version must be 4-part (major.minor.patch.revision). -# Strip pre-release suffix (e.g. 1.0.0-develop.1 -> 1.0.0) then pad to 4 parts. -function ConvertTo-MsixVersion([string]$semver) { - $base = ($semver -split '-')[0] - $parts = $base -split '\.' - while ($parts.Count -lt 4) { $parts += "0" } - ($parts[0..3] -join '.') -} -$Version4 = ConvertTo-MsixVersion $Version - -# ── Architecture / RID ──────────────────────────────────────────────────────── -# MSIX currently supports x64, x86, and arm64 for full-trust desktop apps. -$Rid = "win-x64" -$MsixArch = "x64" -$PackageName = "remote-agent" -$MsixFile = Join-Path $OutDir "${PackageName}_${Version}_${MsixArch}.msix" - -Write-Host "[package-msix] version=$Version ($Version4) rid=$Rid config=$Configuration self-contained=$SelfContained" -Write-Host "[package-msix] service=$BuildService desktop=$BuildDesktop out=$OutDir" - -# ── Locate Windows SDK tools ───────────────────────────────────────────────── -function Find-WinSdkTool([string]$Name) { - $sdkRoot = "C:\Program Files (x86)\Windows Kits\10\bin" - if (Test-Path $sdkRoot) { - Get-ChildItem $sdkRoot -Directory | Sort-Object Name -Descending | ForEach-Object { - $p = Join-Path $_.FullName "x64\$Name" - if (Test-Path $p) { return $p } - } - } - $inPath = Get-Command $Name -ErrorAction SilentlyContinue - if ($inPath) { return $inPath.Source } - return $null -} - -$MakeAppx = Find-WinSdkTool "makeappx.exe" -$SignTool = Find-WinSdkTool "signtool.exe" - -if (-not $MakeAppx) { - Write-Error @" -makeappx.exe not found. Install the Windows SDK: - winget install Microsoft.WindowsSDK.10.0.22621 - -- or -- - https://developer.microsoft.com/windows/downloads/windows-sdk/ -"@ -} - -Write-Host "[package-msix] makeappx : $MakeAppx" -if ($SignTool) { Write-Host "[package-msix] signtool : $SignTool" } - -# ── Ensure output directory ─────────────────────────────────────────────────── -New-Item -ItemType Directory -Path $OutDir -Force | Out-Null - -# ── Publish ─────────────────────────────────────────────────────────────────── -$scFlag = if ($SelfContained) { "--self-contained true" } else { "" } - -if ($BuildService) { - $ServicePub = Join-Path $OutDir "publish-service" - Write-Host "[package-msix] publishing service -> $ServicePub" - $cmd = "dotnet publish `"$(Join-Path $RepoRoot 'src\RemoteAgent.Service\RemoteAgent.Service.csproj')`" " + - "-c $Configuration -r $Rid -f net10.0 --configfile `"$NuGetConfig`" " + - "-o `"$ServicePub`" $scFlag" - Invoke-Expression $cmd - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - # Publish Ollama plugin (always framework-dependent; it loads into the service process). - $OllamaProj = Join-Path $RepoRoot "src\RemoteAgent.Plugins.Ollama\RemoteAgent.Plugins.Ollama.csproj" - if (Test-Path $OllamaProj) { - $PluginPub = Join-Path $OutDir "publish-plugin" - Write-Host "[package-msix] publishing Ollama plugin -> $PluginPub" - $cmd = "dotnet publish `"$OllamaProj`" " + - "-c $Configuration -r $Rid -f net10.0 --configfile `"$NuGetConfig`" " + - "--self-contained false -o `"$PluginPub`"" - Invoke-Expression $cmd - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - } -} - -if ($BuildDesktop) { - $DesktopPub = Join-Path $OutDir "publish-desktop" - Write-Host "[package-msix] publishing desktop -> $DesktopPub" - $cmd = "dotnet publish `"$(Join-Path $RepoRoot 'src\RemoteAgent.Desktop\RemoteAgent.Desktop.csproj')`" " + - "-c $Configuration -r $Rid -f net9.0 --configfile `"$NuGetConfig`" " + - "-o `"$DesktopPub`" $scFlag" - Invoke-Expression $cmd - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -} - -# ── Generate MSIX icon assets ───────────────────────────────────────────────── -# Generates solid-color placeholder PNGs using System.Drawing. Replace with -# real artwork before publishing to the Microsoft Store or any production channel. -function New-IconPng([string]$Path, [int]$W, [int]$H) { - Add-Type -AssemblyName System.Drawing - $bmp = [System.Drawing.Bitmap]::new($W, $H) - $g = [System.Drawing.Graphics]::FromImage($bmp) - $g.Clear([System.Drawing.Color]::FromArgb(0x1E, 0x88, 0xE5)) # Material Blue 600 - try { - $fontSize = [int]([Math]::Max(8, $H * 0.35)) - $font = [System.Drawing.Font]::new("Segoe UI", $fontSize, [System.Drawing.FontStyle]::Bold) - $sf = [System.Drawing.StringFormat]::new() - $sf.Alignment = [System.Drawing.StringAlignment]::Center - $sf.LineAlignment = [System.Drawing.StringAlignment]::Center - $rect = [System.Drawing.RectangleF]::new(0, 0, $W, $H) - $g.DrawString("RA", $font, [System.Drawing.Brushes]::White, $rect, $sf) - $font.Dispose(); $sf.Dispose() - } catch { } # text rendering is best-effort - $g.Dispose() - $bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png) - $bmp.Dispose() -} - -# ── Assemble MSIX package layout ────────────────────────────────────────────── -$PkgRoot = Join-Path $OutDir "msix-layout" -$AssetsDir = Join-Path $PkgRoot "Assets" - -# Clean any previous layout. -if (Test-Path $PkgRoot) { Remove-Item $PkgRoot -Recurse -Force } -New-Item -ItemType Directory -Path $AssetsDir -Force | Out-Null - -# Icons required by the MSIX manifest. -$iconSpecs = @( - @{ Name = "Square44x44Logo.png"; W = 44; H = 44 }, - @{ Name = "Square150x150Logo.png"; W = 150; H = 150 }, - @{ Name = "Wide310x150Logo.png"; W = 310; H = 150 }, - @{ Name = "Square310x310Logo.png"; W = 310; H = 310 }, - @{ Name = "StoreLogo.png"; W = 50; H = 50 } + [switch] $Clean, + [switch] $NoBuild, + [switch] $BumpMajor, + [switch] $BumpMinor, + [switch] $BumpPatch, + [switch] $Install, + [switch] $Force, + [string] $OutDir = '' ) -foreach ($spec in $iconSpecs) { - $iconPath = Join-Path $AssetsDir $spec.Name - - # Try to rasterise the repo SVG with Inkscape or ImageMagick first. - $svgSrc = Join-Path $RepoRoot "src\RemoteAgent.Desktop\Assets\AppIcon\appicon.svg" - $rendered = $false - if (Test-Path $svgSrc) { - $inkscape = Get-Command inkscape -ErrorAction SilentlyContinue - $magick = Get-Command magick -ErrorAction SilentlyContinue - if ($inkscape) { - & inkscape --export-filename="$iconPath" --export-width=$spec.W --export-height=$spec.H "$svgSrc" 2>$null - $rendered = $LASTEXITCODE -eq 0 - } elseif ($magick) { - & magick -background none -resize "$($spec.W)x$($spec.H)" "$svgSrc" "$iconPath" 2>$null - $rendered = $LASTEXITCODE -eq 0 - } - } - - if (-not $rendered) { New-IconPng -Path $iconPath -W $spec.W -H $spec.H } -} -Write-Host "[package-msix] icon assets written to $AssetsDir" - -# Service binaries. -if ($BuildService) { - $ServiceDest = Join-Path $PkgRoot "service" - New-Item -ItemType Directory -Path $ServiceDest -Force | Out-Null - Copy-Item -Path (Join-Path $ServicePub "*") -Destination $ServiceDest -Recurse -Force - - # Include default appsettings so the service has sensible defaults on first run. - $appSettings = Join-Path $RepoRoot "src\RemoteAgent.Service\appsettings.json" - if (Test-Path $appSettings) { - Copy-Item -Path $appSettings -Destination $ServiceDest -Force - } - - # Ollama plugin. - if (Test-Path (Join-Path $OutDir "publish-plugin")) { - $PluginDest = Join-Path $ServiceDest "plugins" - New-Item -ItemType Directory -Path $PluginDest -Force | Out-Null - Get-ChildItem (Join-Path $OutDir "publish-plugin") -Filter "*.dll" | - Copy-Item -Destination $PluginDest -Force - } -} - -# Desktop binaries. -if ($BuildDesktop) { - $DesktopDest = Join-Path $PkgRoot "desktop" - New-Item -ItemType Directory -Path $DesktopDest -Force | Out-Null - Copy-Item -Path (Join-Path $DesktopPub "*") -Destination $DesktopDest -Recurse -Force -} - -# ── Write AppxManifest.xml ──────────────────────────────────────────────────── -# The manifest always has one Application entry (desktop app, or a headless stub -# when -ServiceOnly is set). The windows.service Extension is added when the -# service component is included. -$desktopExe = "desktop\RemoteAgent.Desktop.exe" -$serviceExe = "service\RemoteAgent.Service.exe" - -# Application element: use desktop exe if available, else service exe as stub. -$appExe = if ($BuildDesktop) { $desktopExe } else { $serviceExe } -$appDisplayName = if ($BuildDesktop) { "Remote Agent Desktop" } else { "Remote Agent Service" } -$appDescription = if ($BuildDesktop) { - "Remote Agent desktop management application" -} else { - "Remote Agent gRPC service host" -} - -$serviceExtensionXml = "" -if ($BuildService) { - $serviceExtensionXml = @" - - - - - - -"@ -} - -$rescapNs = 'xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"' -$desktop6Ns = if ($BuildService) { 'xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"' } else { "" } -$ignorable = if ($BuildService) { 'IgnorableNamespaces="rescap desktop6"' } else { 'IgnorableNamespaces="rescap"' } - -$manifest = @" - - - - - - - Remote Agent - Remote Agent - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - - - - -$serviceExtensionXml - -"@ - -$manifestPath = Join-Path $PkgRoot "AppxManifest.xml" -$manifest | Set-Content -Path $manifestPath -Encoding UTF8 -Write-Host "[package-msix] wrote AppxManifest.xml" - -# ── Run makeappx ────────────────────────────────────────────────────────────── -Write-Host "[package-msix] packing $MsixFile ..." -& $MakeAppx pack /d "$PkgRoot" /p "$MsixFile" /o /nv -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -Write-Host "[package-msix] packed: $MsixFile" - -# ── Sign ────────────────────────────────────────────────────────────────────── -$signThumb = $CertThumbprint - -if ($DevCert -and -not $signThumb) { - if (-not $SignTool) { - Write-Warning "signtool.exe not found — skipping signing. Install Windows SDK to enable signing." - } else { - # Reuse existing dev cert if one with the same subject already exists. - $existing = Get-ChildItem Cert:\CurrentUser\My | - Where-Object { $_.Subject -eq $Publisher -and $_.NotAfter -gt (Get-Date) } | - Sort-Object NotAfter -Descending | - Select-Object -First 1 - - if ($existing) { - Write-Host "[package-msix] reusing existing dev cert: $($existing.Thumbprint)" - $signThumb = $existing.Thumbprint - } else { - Write-Host "[package-msix] creating self-signed dev certificate for '$Publisher'..." - $cert = New-SelfSignedCertificate ` - -Type Custom ` - -Subject $Publisher ` - -KeyUsage DigitalSignature ` - -FriendlyName "Remote Agent Dev Certificate" ` - -CertStoreLocation "Cert:\CurrentUser\My" ` - -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") - $signThumb = $cert.Thumbprint - Write-Host "[package-msix] dev cert thumbprint: $signThumb" - - # Export the public certificate so it can be installed as a trusted root - # on the target machine before running Add-AppxPackage. - $cerPath = Join-Path $OutDir "remote-agent-dev.cer" - Export-Certificate -Cert $cert -FilePath $cerPath -Type CERT | Out-Null - Write-Host "[package-msix] exported public cert: $cerPath" - Write-Host "[package-msix] To trust on target machine:" - Write-Host " Import-Certificate -FilePath '$cerPath' -CertStoreLocation Cert:\LocalMachine\Root" - } - } -} - -if ($signThumb -and $SignTool) { - Write-Host "[package-msix] signing with thumbprint $signThumb ..." - & $SignTool sign /sha1 $signThumb /fd SHA256 /tr http://timestamp.digicert.com /td sha256 "$MsixFile" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Write-Host "[package-msix] signed: $MsixFile" -} elseif (-not $signThumb) { - Write-Warning "Package is unsigned. To install it locally, use:" - Write-Warning " Add-AppxPackage -Path '$MsixFile' -AllowUnsigned" -} - -# ── Summary ──────────────────────────────────────────────────────────────────── -$msixSize = [math]::Round((Get-Item $MsixFile).Length / 1MB, 1) -$installPs1 = Join-Path $PSScriptRoot "install-remote-agent.ps1" -$cerArg = if ($DevCert -and (Test-Path (Join-Path $OutDir "remote-agent-dev.cer"))) { - " -CertPath '$(Join-Path $OutDir 'remote-agent-dev.cer')'" -} else { "" } - -Write-Host "" -Write-Host "── MSIX package ready ──────────────────────────────────────────────────" -Write-Host " File : $MsixFile ($msixSize MB)" -Write-Host " Identity : RemoteAgent $Version4 $MsixArch" -Write-Host " Publisher: $Publisher" -if ($BuildService) { Write-Host " Service : service\RemoteAgent.Service.exe (RemoteAgentService, Automatic, LocalSystem)" } -if ($BuildDesktop) { Write-Host " Desktop : desktop\RemoteAgent.Desktop.exe (Start menu: Remote Agent Desktop)" } -Write-Host "" -Write-Host " Install + start service (run as Administrator):" -Write-Host " .\scripts\install-remote-agent.ps1$cerArg" -Write-Host " -- or manually --" -if ($signThumb) { - Write-Host " Add-AppxPackage -Path '$MsixFile'" -} else { - Write-Host " Add-AppxPackage -Path '$MsixFile' -AllowUnsigned" -} -if ($BuildService) { - Write-Host " Start-Service RemoteAgentService" -} -Write-Host "" -Write-Host " Uninstall:" -Write-Host " .\scripts\install-remote-agent.ps1 -Uninstall" -Write-Host "────────────────────────────────────────────────────────────────────────" +$ErrorActionPreference = 'Stop' +$RepoRoot = (Get-Item $PSScriptRoot).Parent.FullName +Import-Module (Join-Path $PSScriptRoot 'MsixTools\MsixTools.psd1') -Force + +$params = @{ + WorkspaceRoot = $RepoRoot + ConfigPath = Join-Path $RepoRoot 'msix.yml' + Configuration = $Configuration + SelfContained = $SelfContained + Clean = $Clean + NoBuild = $NoBuild + BumpMajor = $BumpMajor + BumpMinor = $BumpMinor + BumpPatch = $BumpPatch + Force = $Force + Install = $Install +} +if ($Version) { $params['Version'] = $Version } +if ($Publisher) { $params['Publisher'] = $Publisher } +if ($CertThumbprint) { $params['CertThumbprint'] = $CertThumbprint } +if ($DevCert) { $params['DevCert'] = $true } +if ($OutDir) { $params['OutDir'] = $OutDir } +if ($ServiceOnly) { $params['ExcludeDesktop'] = $true } +if ($DesktopOnly) { $params['ExcludeService'] = $true } + +New-MsixPackage @params + +# Explicitly exit 0 to prevent $LASTEXITCODE set by any native command inside +# New-MsixPackage (dotnet, makeappx, signtool) from causing the GitHub Actions +# pwsh runner to report failure even when the package was created successfully. +exit 0 diff --git a/scripts/remote-agent-ctl.sh b/scripts/remote-agent-ctl.sh new file mode 100644 index 0000000..23c272f --- /dev/null +++ b/scripts/remote-agent-ctl.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# remote-agent-ctl — Start, stop, restart, or check the Remote Agent gRPC service. +# +# Works with both systemd and non-systemd environments (WSL, Pengwin, etc.). +# When systemd is active, delegates to systemctl. Otherwise uses start-stop-daemon +# directly, tracking the process via /run/remote-agent.pid. +# +# Usage: +# remote-agent-ctl {start|stop|restart|status} + +set -euo pipefail + +SVC_NAME="remote-agent.service" +SVC_BIN="/usr/lib/remote-agent/service/RemoteAgent.Service" +SVC_USER="remote-agent" +PID_FILE="/run/remote-agent.pid" +LOG_FILE="/var/log/remote-agent/service.log" +ENV_FILE="/etc/remote-agent/environment" + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +_has_systemd() { + [ -d /run/systemd/system ] +} + +_source_env() { + if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + . "$ENV_FILE" 2>/dev/null || true + set +a + fi + export ASPNETCORE_CONTENTROOT=/etc/remote-agent +} + +_is_running() { + # Check PID file first. + if [ -f "$PID_FILE" ]; then + _pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "$_pid" ] && kill -0 "$_pid" 2>/dev/null; then + return 0 + fi + fi + # Fallback: check if the binary is already running (e.g. started at boot + # outside of this script, so no PID file was written by us). + if pgrep -x "$(basename "$SVC_BIN")" > /dev/null 2>&1; then + # Capture the PID and update the PID file so future calls use it. + _live_pid=$(pgrep -x "$(basename "$SVC_BIN")" | head -1) + echo "$_live_pid" | sudo tee "$PID_FILE" > /dev/null 2>/dev/null || true + return 0 + fi + return 1 +} + +# ── Commands ────────────────────────────────────────────────────────────────── + +_start() { + if _has_systemd; then + systemctl start "$SVC_NAME" + return + fi + + if _is_running; then + echo "remote-agent is already running (pid $(cat "$PID_FILE" 2>/dev/null || pgrep -x "$(basename "$SVC_BIN")" | head -1))." + return 0 + fi + + if [ ! -x "$SVC_BIN" ]; then + echo "ERROR: service binary not found: $SVC_BIN" >&2 + exit 1 + fi + + _source_env + mkdir -p "$(dirname "$LOG_FILE")" + + echo "Starting remote-agent via start-stop-daemon..." + start-stop-daemon --start --background \ + --make-pidfile --pidfile "$PID_FILE" \ + --chuid "$SVC_USER" \ + --output "$LOG_FILE" \ + --exec "$SVC_BIN" + + # Give the process a moment to start. + sleep 1 + + if _is_running; then + echo "remote-agent started (pid $(cat "$PID_FILE"))." + else + echo "ERROR: remote-agent failed to start. Check $LOG_FILE" >&2 + exit 1 + fi +} + +_stop() { + if _has_systemd; then + systemctl stop "$SVC_NAME" + return + fi + + if ! _is_running; then + echo "remote-agent is not running." + return 0 + fi + + echo "Stopping remote-agent..." + start-stop-daemon --stop --retry 5 \ + --pidfile "$PID_FILE" \ + --exec "$SVC_BIN" 2>/dev/null || true + rm -f "$PID_FILE" + echo "remote-agent stopped." +} + +_restart() { + if _has_systemd; then + systemctl restart "$SVC_NAME" + return + fi + + _stop + sleep 1 + _start +} + +_status() { + if _has_systemd; then + systemctl status "$SVC_NAME" + return + fi + + if _is_running; then + echo "remote-agent is running (pid $(cat "$PID_FILE"))." + else + echo "remote-agent is not running." + exit 1 + fi +} + +# ── Dispatch ────────────────────────────────────────────────────────────────── + +case "${1:-}" in + start) _start ;; + stop) _stop ;; + restart) _restart ;; + status) _status ;; + *) + echo "Usage: $(basename "$0") {start|stop|restart|status}" >&2 + exit 1 + ;; +esac diff --git a/scripts/set-pairing-user.ps1 b/scripts/set-pairing-user.ps1 new file mode 100644 index 0000000..4542b56 --- /dev/null +++ b/scripts/set-pairing-user.ps1 @@ -0,0 +1,165 @@ +<# +.SYNOPSIS + Adds or updates a pairing user in the RemoteAgent service appsettings.json. + +.PARAMETER Username + The pairing user's login name (case-insensitive match for upsert). + +.PARAMETER Password + The plaintext password. The SHA-256 hex digest is stored, never the plaintext. + +.PARAMETER Replace + When specified, replaces the entire PairingUsers array with only this user. + Without this switch the entry is upserted (added if not present, updated if it exists). + +.EXAMPLE + .\set-pairing-user.ps1 -Username alice -Password s3cr3t + .\set-pairing-user.ps1 -Username alice -Password newpassword -Replace +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string] $Username, + + [Parameter(Mandatory = $true)] + [string] $Password, + + [switch] $Replace +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# --------------------------------------------------------------------------- +# 1. Locate appsettings.json +# --------------------------------------------------------------------------- +function Find-AppSettings { + # Try to find the service executable path via SC query + try { + $scOutput = & sc.exe qc RemoteAgentService 2>$null + if ($LASTEXITCODE -eq 0) { + $binaryLine = $scOutput | Where-Object { $_ -match 'BINARY_PATH_NAME' } + if ($binaryLine) { + $exePath = ($binaryLine -replace '.*BINARY_PATH_NAME\s+:\s+', '').Trim() + # Strip any CLI arguments (path ends at the first unquoted space after the exe) + $exePath = $exePath -replace '^"([^"]+)".*', '$1' + $exePath = $exePath -replace '^(\S+).*', '$1' + $serviceDir = Split-Path -Parent $exePath + $candidate = Join-Path $serviceDir 'appsettings.json' + if (Test-Path $candidate) { + return $candidate + } + } + } + } + catch { + # SC query failed – ignore and fall through + } + + # Default installed location + $defaultPath = 'C:\Program Files\WindowsApps\RemoteAgent\service\appsettings.json' + if (Test-Path $defaultPath) { + return $defaultPath + } + + # Fall back to repo source (development) + $repoFallback = Join-Path $PSScriptRoot '..' 'src' 'RemoteAgent.Service' 'appsettings.json' + $repoFallback = [System.IO.Path]::GetFullPath($repoFallback) + if (Test-Path $repoFallback) { + return $repoFallback + } + + throw "Could not find appsettings.json. Searched:`n $defaultPath`n $repoFallback" +} + +# --------------------------------------------------------------------------- +# 2. Compute SHA-256 hex of the password +# --------------------------------------------------------------------------- +$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($Password) +$hashBytes = [System.Security.Cryptography.SHA256]::HashData($passwordBytes) +$passwordHash = ($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '' + +Write-Verbose "Password SHA-256: $passwordHash" + +# --------------------------------------------------------------------------- +# 3. Find and read appsettings.json +# --------------------------------------------------------------------------- +$appSettingsPath = Find-AppSettings +Write-Host "Using appsettings.json: $appSettingsPath" + +$json = Get-Content -Path $appSettingsPath -Raw -Encoding UTF8 +$settings = $json | ConvertFrom-Json -AsHashtable + +# --------------------------------------------------------------------------- +# 4. Upsert (or replace) the pairing user entry +# --------------------------------------------------------------------------- +if (-not $settings.ContainsKey('Agent')) { + $settings['Agent'] = @{} +} + +$agent = $settings['Agent'] + +if ($Replace) { + # Replace entire array with only this user + $agent['PairingUsers'] = @( + [ordered]@{ Username = $Username; PasswordHash = $passwordHash } + ) +} +else { + # Upsert: replace existing entry matched case-insensitively, or append + $existingUsers = if ($agent.ContainsKey('PairingUsers') -and $agent['PairingUsers']) { + @($agent['PairingUsers']) + } + else { + @() + } + + $replaced = $false + $newUsers = @() + foreach ($user in $existingUsers) { + $uName = if ($user -is [hashtable]) { $user['Username'] } else { $user.Username } + if ([string]::Equals($uName, $Username, [System.StringComparison]::OrdinalIgnoreCase)) { + $newUsers += [ordered]@{ Username = $Username; PasswordHash = $passwordHash } + $replaced = $true + } + else { + $newUsers += $user + } + } + + if (-not $replaced) { + $newUsers += [ordered]@{ Username = $Username; PasswordHash = $passwordHash } + } + + $agent['PairingUsers'] = $newUsers +} + +$settings['Agent'] = $agent + +# --------------------------------------------------------------------------- +# 5. Write appsettings.json back with indentation +# --------------------------------------------------------------------------- +$updatedJson = $settings | ConvertTo-Json -Depth 10 +Set-Content -Path $appSettingsPath -Value $updatedJson -Encoding UTF8 + +Write-Host "SUCCESS: Pairing user '$Username' has been set in $appSettingsPath" + +# --------------------------------------------------------------------------- +# 6. Restart RemoteAgentService (non-fatal if not found) +# --------------------------------------------------------------------------- +try { + $svc = Get-Service -Name 'RemoteAgentService' -ErrorAction SilentlyContinue + if ($svc) { + Write-Host "Stopping RemoteAgentService..." + Stop-Service -Name 'RemoteAgentService' -Force + Write-Host "Starting RemoteAgentService..." + Start-Service -Name 'RemoteAgentService' + Write-Host "RemoteAgentService restarted." + } + else { + Write-Verbose "RemoteAgentService not found – skipping service restart." + } +} +catch { + Write-Warning "Could not restart RemoteAgentService: $_" +} diff --git a/src/RemoteAgent.App.Logic/AgentSessionClient.cs b/src/RemoteAgent.App.Logic/AgentSessionClient.cs index 4304f13..d230dbb 100644 --- a/src/RemoteAgent.App.Logic/AgentSessionClient.cs +++ b/src/RemoteAgent.App.Logic/AgentSessionClient.cs @@ -13,6 +13,7 @@ public interface IAgentSessionClient : IAgentInteractionSession string? PerRequestContext { get; set; } event Action? ConnectionStateChanged; event Action? MessageReceived; + event Action? FileTransferReceived; Task ConnectAsync( string host, @@ -47,6 +48,7 @@ public sealed class AgentSessionClient(Func? mediaTextFormat public event Action? ConnectionStateChanged; public event Action? MessageReceived; + public event Action? FileTransferReceived; public async Task ConnectAsync( string host, @@ -207,6 +209,23 @@ private async Task ReceiveLoop(CancellationToken ct) while (await _call.ResponseStream.MoveNext(ct)) { var incoming = _call.ResponseStream.Current; + + // Handle file transfers separately: raise dedicated event + chat notification. + if (incoming.PayloadCase == ServerMessage.PayloadOneofCase.FileTransfer && incoming.FileTransfer != null) + { + var ft = incoming.FileTransfer; + FileTransferReceived?.Invoke(ft); + var sizeText = ft.TotalSize < 1024 ? $"{ft.TotalSize} B" : $"{ft.TotalSize / 1024.0:F1} KB"; + var chatMsg = new ChatMessage + { + IsUser = false, + Text = $"\U0001F4C1 File received: {ft.RelativePath} ({sizeText})", + FileTransferPath = ft.RelativePath + }; + MessageReceived?.Invoke(chatMsg); + continue; + } + var mapped = MapServerMessage(incoming); if (mapped != null) MessageReceived?.Invoke(mapped); diff --git a/src/RemoteAgent.App.Logic/ChatMessage.cs b/src/RemoteAgent.App.Logic/ChatMessage.cs index 05f7338..b5aca54 100644 --- a/src/RemoteAgent.App.Logic/ChatMessage.cs +++ b/src/RemoteAgent.App.Logic/ChatMessage.cs @@ -39,6 +39,9 @@ public class ChatMessage : INotifyPropertyChanged /// Message priority (FR-3.1). Notify causes a system notification; tap opens the app (FR-3.2, FR-3.3). public ChatMessagePriority Priority { get; init; } = ChatMessagePriority.Normal; + /// When set, indicates this message represents a file transfer. Contains the relative path of the transferred file. + public string? FileTransferPath { get; init; } + /// Plain text for display: event message or raw text (no markdown). public string DisplayText => IsEvent ? (EventMessage ?? "") : Text; diff --git a/src/RemoteAgent.App.Logic/ISessionCommandBus.cs b/src/RemoteAgent.App.Logic/ISessionCommandBus.cs index 8e818a8..71b2de2 100644 --- a/src/RemoteAgent.App.Logic/ISessionCommandBus.cs +++ b/src/RemoteAgent.App.Logic/ISessionCommandBus.cs @@ -10,4 +10,6 @@ public interface ISessionCommandBus bool SelectSession(string? sessionId); Task TerminateSessionAsync(string? sessionId); string? GetCurrentSessionId(); + bool IsConnected { get; } + event Action? ConnectionStateChanged; } diff --git a/src/RemoteAgent.App.Logic/PlatformAbstractions.cs b/src/RemoteAgent.App.Logic/PlatformAbstractions.cs index d25cc3a..65b2141 100644 --- a/src/RemoteAgent.App.Logic/PlatformAbstractions.cs +++ b/src/RemoteAgent.App.Logic/PlatformAbstractions.cs @@ -2,11 +2,6 @@ namespace RemoteAgent.App.Logic; -public interface IConnectionModeSelector -{ - Task SelectAsync(); -} - public interface IAgentSelector { Task SelectAsync(ServerInfoResponse serverInfo); @@ -38,3 +33,18 @@ public interface INotificationService { void Show(string title, string body); } + +/// Opens the server login page and extracts the pairing deep-link URI after the user authenticates, or returns null if cancelled. +public interface IQrCodeScanner +{ + Task ScanAsync(string loginUrl); +} + +/// Routes deep-link URIs arriving from the OS (e.g. remoteagent://pair?…) to subscribers. Queues one URI if dispatched before any subscriber registers. +public interface IDeepLinkService +{ + /// Subscribe and immediately receive any queued pending URI. + void Subscribe(Action handler); + /// Dispatch a URI to all subscribers; queues it if there are none yet. + void Dispatch(string rawUri); +} diff --git a/src/RemoteAgent.App.Logic/ServerApiClient.cs b/src/RemoteAgent.App.Logic/ServerApiClient.cs index 9a79f6e..4fb31e4 100644 --- a/src/RemoteAgent.App.Logic/ServerApiClient.cs +++ b/src/RemoteAgent.App.Logic/ServerApiClient.cs @@ -1,19 +1,12 @@ using Grpc.Core; using Grpc.Net.Client; using RemoteAgent.Proto; -using System.Net.Http.Json; -using System.Text.Json; namespace RemoteAgent.App.Logic; /// Shared server interaction APIs used by mobile and desktop clients. public static class ServerApiClient { - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNameCaseInsensitive = true - }; - public static Task GetServerInfoAsync( string host, int port, @@ -276,27 +269,30 @@ public static async Task MonitorStructuredLogsAsync( CancellationToken ct = default, bool throwOnError = false) { - var baseUrl = BuildBaseUrl(host, port).TrimEnd('/'); - var query = string.IsNullOrWhiteSpace(agentId) - ? "" - : $"?agentId={Uri.EscapeDataString(agentId.Trim())}"; - var url = $"{baseUrl}/api/sessions/capacity{query}"; - - using var client = new HttpClient(); - if (!string.IsNullOrWhiteSpace(apiKey)) - client.DefaultRequestHeaders.Add("x-api-key", apiKey.Trim()); - - using var response = await client.GetAsync(url, ct); - if (!response.IsSuccessStatusCode) - { - if (!throwOnError) - return null; + var response = await ExecuteGrpcAsync( + host, + port, + apiKey, + "Check session capacity", + throwOnError, + ct, + (client, headers, token) => client.CheckSessionCapacityAsync( + new CheckSessionCapacityRequest { AgentId = agentId ?? "" }, + headers, + cancellationToken: token).ResponseAsync); - var detail = await TryReadErrorDetailAsync(response, ct); - throw CreateHttpFailure("Get session capacity", response.StatusCode, response.ReasonPhrase, detail); - } + if (response == null) return null; - return await response.Content.ReadFromJsonAsync(JsonOptions, ct); + return new SessionCapacitySnapshot( + response.CanCreateSession, + response.Reason, + response.MaxConcurrentSessions, + response.ActiveSessionCount, + response.RemainingServerCapacity, + response.AgentId, + response.HasAgentLimit ? response.AgentMaxConcurrentSessions : null, + response.AgentActiveSessionCount, + response.HasAgentLimit ? response.RemainingAgentCapacity : null); } private static async Task ExecuteGrpcAsync( @@ -349,51 +345,59 @@ private static InvalidOperationException CreateGrpcFailure(string operation, Rpc return new InvalidOperationException($"{operation} failed ({ex.StatusCode}): {detail}", ex); } - private static InvalidOperationException CreateHttpFailure(string operation, System.Net.HttpStatusCode statusCode, string? reasonPhrase, string? detail) - { - var code = (int)statusCode; - var reason = string.IsNullOrWhiteSpace(reasonPhrase) ? "HTTP error" : reasonPhrase; - var message = string.IsNullOrWhiteSpace(detail) - ? $"{operation} failed ({code} {reason})." - : $"{operation} failed ({code} {reason}): {detail}"; - return new InvalidOperationException(message); - } - - private static async Task TryReadErrorDetailAsync(HttpResponseMessage response, CancellationToken ct) - { - try - { - var body = await response.Content.ReadAsStringAsync(ct); - if (string.IsNullOrWhiteSpace(body)) - return null; - - using var doc = JsonDocument.Parse(body); - if (doc.RootElement.ValueKind == JsonValueKind.Object && - doc.RootElement.TryGetProperty("message", out var messageNode) && - messageNode.ValueKind == JsonValueKind.String) - { - return messageNode.GetString(); - } - - return body.Length <= 200 ? body : body[..200]; - } - catch - { - return null; - } - } - public static Metadata? CreateHeaders(string? apiKey) { if (string.IsNullOrWhiteSpace(apiKey)) return null; return new Metadata { { "x-api-key", apiKey.Trim() } }; } - public static string BuildBaseUrl(string host, int port) - { - return port == 443 ? $"https://{host}" : $"http://{host}:{port}"; - } -} + public static string BuildBaseUrl(string host, int port) + { + return port == 443 ? $"https://{host}" : $"http://{host}:{port}"; + } + + public static Task ListAgentRunnersAsync( + string host, + int port, + string? apiKey = null, + CancellationToken ct = default, + bool throwOnError = false) + => ExecuteGrpcAsync( + host, + port, + apiKey, + "List agent runners", + throwOnError, + ct, + (client, headers, token) => client.ListAgentRunnersAsync(new ListAgentRunnersRequest(), headers, deadline: null, cancellationToken: token).ResponseAsync); + + public static Task UpdateAgentRunnerAsync( + string host, + int port, + string runnerId, + string? command = null, + string? arguments = null, + int maxConcurrentSessions = 0, + bool setAsDefault = false, + string? apiKey = null, + CancellationToken ct = default, + bool throwOnError = false) + => ExecuteGrpcAsync( + host, + port, + apiKey, + "Update agent runner", + throwOnError, + ct, + (client, headers, token) => client.UpdateAgentRunnerAsync(new UpdateAgentRunnerRequest + { + RunnerId = runnerId, + Command = command ?? "", + Arguments = arguments ?? "", + MaxConcurrentSessions = maxConcurrentSessions, + SetAsDefault = setAsDefault + }, headers, deadline: null, cancellationToken: token).ResponseAsync); +} public sealed record SessionCapacitySnapshot( bool CanCreateSession, diff --git a/src/RemoteAgent.App.Logic/ServerProfile.cs b/src/RemoteAgent.App.Logic/ServerProfile.cs new file mode 100644 index 0000000..081e8dd --- /dev/null +++ b/src/RemoteAgent.App.Logic/ServerProfile.cs @@ -0,0 +1,45 @@ +namespace RemoteAgent.App.Logic; + +/// +/// A saved server profile keyed by Host + Port. +/// Stores per-server configuration that applies to new connections and sessions. +/// +public sealed class ServerProfile +{ + /// Server hostname or IP address. + public string Host { get; set; } = ""; + + /// Server gRPC port. + public int Port { get; set; } = 5244; + + /// API key for authentication (may be empty if not required). + public string ApiKey { get; set; } = ""; + + /// Optional user-friendly display name for the server. + public string DisplayName { get; set; } = ""; + + /// Text prepended to every chat message sent to the agent. + public string PerRequestContext { get; set; } = ""; + + /// Default context seeded into new sessions on this server. + public string DefaultSessionContext { get; set; } = ""; +} + +/// +/// Persistence for saved server profiles. Implementations should treat +/// Host + Port as the unique key. +/// +public interface IServerProfileStore +{ + /// Return all saved profiles. + IReadOnlyList GetAll(); + + /// Find a profile by host and port, or null if not saved. + ServerProfile? GetByHostPort(string host, int port); + + /// Insert or update a profile (matched by Host + Port). + void Upsert(ServerProfile profile); + + /// Delete a profile by host and port. + bool Delete(string host, int port); +} diff --git a/src/RemoteAgent.App.Logic/ViewModels/AppShellViewModel.cs b/src/RemoteAgent.App.Logic/ViewModels/AppShellViewModel.cs index 3329b3e..1891681 100644 --- a/src/RemoteAgent.App.Logic/ViewModels/AppShellViewModel.cs +++ b/src/RemoteAgent.App.Logic/ViewModels/AppShellViewModel.cs @@ -28,6 +28,12 @@ public AppShellViewModel( NavigateToAccountCommand = new RelayCommand(() => _ = NavigateAsync("//AccountManagementPage")); OpenSessionsCommand = new RelayCommand(() => _ = OpenSessionsAsync()); + _sessionBus.ConnectionStateChanged += () => + { + OnPropertyChanged(nameof(IsConnected)); + RefreshSessions(); + }; + RefreshSessions(); } @@ -35,6 +41,8 @@ public AppShellViewModel( public ObservableCollection SessionItems { get; } = []; + public bool IsConnected => _sessionBus.IsConnected; + public ICommand StartSessionCommand { get; } public ICommand SelectSessionCommand { get; } public ICommand TerminateSessionCommand { get; } diff --git a/src/RemoteAgent.App/AppShell.xaml b/src/RemoteAgent.App/AppShell.xaml index 66f550d..139252d 100644 --- a/src/RemoteAgent.App/AppShell.xaml +++ b/src/RemoteAgent.App/AppShell.xaml @@ -11,8 +11,8 @@