Skip to content

Add Multiple Servers: bulk paste-a-list onboarding with shared credentials (both apps) - #1549

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/bulk-server-onboarding
Jul 17, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
feature/bulk-server-onboarding

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Problem

Adding a monitoring estate one server at a time is the onboarding wall: a 40-box fleet meant opening the single Add-Server dialog forty times and re-typing the same SQL login into every one. Both apps needed a way to paste a list and add it all at once — with the shared credential entered a single time, not forty.

Design

An "Add Multiple Servers" button beside Add Server in both Lite and the Darling viewer opens a paste-a-list dialog.

  • Shared grammar — a new PerformanceMonitor.Common.BulkServerListParser parses server[, display name][, database] per line: comma- or tab-separated (tab wins when present, so a spreadsheet paste keeps commas inside a field), trimmed, trailing empty fields dropped (Excel's trailing cells), # comments and blank lines skipped with line numbers still correct, and empty-server / too-many-fields reported as per-line errors rather than silently dropped. Pure and synchronous; the parser does no dedupe or normalization (both are app-side by design). A mirrored test file in both suites pins the grammar so the two apps can't drift.
  • One secret, never N — the shared auth block is entered once. Lite mints a single auto-named CredentialProfile (unique-suffixed on a name clash) that every added row references, created only when at least one new row survives dedupe (a fully-duplicate paste leaves no orphan profile). Darling has no store-side profile table, so it resolves the shared credential once to a single DPAPI-LocalMachine blob — ViewerServerSecret.Protect called exactly once — and stamps that same blob onto every server row and every test_connect probe. No plaintext credential is ever written.
  • Duplicates skipped, not silently merged — both apps gate candidates through the ratified HashSet<string>(OrdinalIgnoreCase) over the shared ServerIdHelper.BuildStorageName(name, db, readOnlyIntent) identity, seeded from existing servers' real keys and derived from the built row (one composition feeds the gate and the stored server_id). Case-folding is the point: an existing SQL01 skips a pasted sql01 even though their raw hashes differ, while a per-database Azure entry and a read-only-intent connection stay distinct. A blank database maps to NULL, never master — coercing it would mint a mismatched identity and split a server's collected data from its single-added twin.
  • Test All is informational only (no server is added) and honest per app: Lite probes directly, so it runs up to 8 in parallel, cancellable, with each probe catching every error internally (an escaping OperationCanceledException on Cancel would crash the app — the never-faulting probe structure forbids it) and honoring the configured connection timeout. Darling routes probes through the service's test_connect command, and because the service drains its queue serially, Test All is a sequential sweep with live per-row status and a Cancel that stops between rows — the in-flight probe finishes and self-deletes its credential-bearing command row, so no orphaned rows and no parallelism theater.
  • Entra MFA is excluded from bulk in both apps (an interactive popup per server is a popup storm at first collection), and a defensive belt in each app's row-mapping helper rejects any resolved un-honorable auth at the single choke point feeding both Test All and Add — so it can't leak in through a hand-edited profile.

Add writes every valid non-duplicate row (per-row failures reported, batch continues), returns an added / skipped / failed summary, and does not auto-connect (the background collector picks new rows up on its next cycle).

Parity

Both apps get the same feature from the same shared parser and the same dedupe identity. The two documented asymmetries are intentional and match each app's architecture: Lite mints one shared credential profile while Darling stamps one shared DPAPI blob (the store has no profile concept), and Lite's Test All is genuinely parallel while Darling's is sequential (the service drains commands serially, so viewer-side parallelism would be theater). The single Add-Server dialog is not edited in either app (it remains the MFA path and keeps the per-server niceties bulk omits), and the deprecated Full Dashboard is untouched.

Test results

  • dotnet build (Debug): Lite 0 errors, Darling viewer 0 errors.
  • Full Lite.Tests: 1352 passed, 0 failed (incl. the mirrored parser cases, the BuildServerConnection mapping + dedupe-gate pins, and ThemeCompletenessTests 2/2).
  • Full Darling.Tests: 2148 passed, 134 skipped (live-Postgres-gated), 0 failed (incl. the mirrored parser cases, the BuildMonitoredServerRow / BuildTestConnectServer mapping + dedupe-gate pins, and ThemeCompletenessTests 2/2).
  • No capability-pin baselines changed (the ServerTab no-DataGridFilterManager scan and the plan-viewer ratchet stay green).
  • EOL check: git diff --ignore-cr-at-eol --name-only (6) equals git diff --name-only (6); the 8-file gap to git status --short (14) is exactly the new untracked files.

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits July 17, 2026 00:24
…tials (both apps)

Adds an "Add Multiple Servers" entry beside Add Server in Lite and the Darling
viewer: paste a server list (server[, display][, database]), set one shared
auth + encryption block, optionally Test All, and add every non-duplicate row
at once. Motivated by the onboarding wall of adding a 40-server estate one
dialog at a time, re-typing the same login into each.

- Shared parser (PerformanceMonitor.Common/BulkServerListParser.cs): the
  normative comma/tab grammar with #-comments, trailing-field tolerance, and
  per-line errors, mirrored-tested in both suites so the apps can't drift.
- One secret, never N: Lite mints a single auto-named CredentialProfile every
  row references (created only when >=1 new row survives dedupe); Darling
  resolves the shared credential once to a single DPAPI blob (Protect called
  exactly once) stamped on every row. No plaintext anywhere.
- Duplicates skipped via the ratified OrdinalIgnoreCase gate over the shared
  ServerIdHelper storage name, keyed from the BUILT row (one composition feeds
  the gate and the stored id); blank database maps to NULL, never "master".
- Test All is informational only: Lite runs bounded-8 parallel, never-faulting,
  cancellable probes; Darling runs an honest sequential test_connect sweep (the
  service drains its command queue serially) with stop-between-rows cancel and
  no orphaned command rows.
- Entra MFA is excluded from bulk (an interactive popup per server is a popup
  storm); a mapping-helper belt in each app rejects any resolved un-honorable
  auth at the single choke point feeding both Test All and Add.

The single Add-Server dialog is unchanged (still the MFA path); the deprecated
Full Dashboard is untouched. Full Lite.Tests (1352) and Darling.Tests (2148 +
134 skipped) pass, including both ThemeCompletenessTests and all capability pins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase-6 review finding (cosmetic): the Test-All run freezes the paste box
so a TextChanged re-parse can't rebuild the row VMs under in-flight status
writes, but the async Add path left it editable -- a mid-add edit would
orphan MarkRow's failure statuses onto discarded VMs (adds themselves are
unaffected; they work from the parse snapshot). Freeze it during Add too,
symmetric with Test All. Lite needs nothing: its add is fully synchronous,
so the reentrancy window does not exist there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit e8d7303 into dev Jul 17, 2026
2 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/bulk-server-onboarding branch July 17, 2026 04:54
pull Bot pushed a commit to ehtick/PerformanceMonitor that referenced this pull request Jul 22, 2026
…erver)

Add the server-onboarding MCP write tools so an MCP client can stand up or
tear down FLEET monitoring conversationally - the service-side twin of the
Viewer's Add / Manage Servers dialogs, and the sibling of the erikdarlingdata#1600 Custom
Views and erikdarlingdata#1608 alert-tuning MCP write tools.

- add_servers (BULK): a JSON array of server objects, processed sequentially -
  validate -> case-folded dedupe (shared ServerIdHelper.BuildStorageName gate,
  erikdarlingdata#1549) -> in-process DarlingServerConnector.ProbeAsync (the service holds the
  network path + credentials, so no test_connect command plane) -> DarlingSecrets
  DPAPI-encrypt (round-trips at collection) -> INSERT mirroring
  SeedMonitoredServersAsync. Per-server added/duplicate/connection_failed/invalid;
  a connection failure does not abort the batch; Entra/MFA rejected; encrypt_mode
  + trust_server_certificate exposed. Returns {added, skipped, failed, results}.
- remove_server: resolve via the same resolver the read tools use, DELETE the
  config_monitored_servers row.
- Grant: mcp role gets INSERT/UPDATE/DELETE on config.config_monitored_servers
  (provisioning, not migration); the encrypted_password column stays SELECT-carved
  (write a credential, never read one back). The erikdarlingdata#1608 config_service beacon
  column-grant already covers the monitored-servers bump trigger.
- MCP instructions (85 -> 87 tools), cross-app ratchet (Darling-only), /api/read
  write-exclusion, and the README MCP blast-radius (credential-on-the-wire note).

Verified: Darling service + Darling.Tests build clean; Darling.Tests 2847 passed
/ 0 failed / 146 gated-live skipped (DARLING_TEST_PG cleared); Lite.Tests
CrossAppMcpToolInventoryPinTests 2 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant