feat(web): manual assets W04 — third device class, add/edit modal, e2e, docs (#4622) - #5261
Conversation
… modal, e2e, docs (#4622) Surfaces manual_assets (W02/W01) as a third class in the unified Devices list alongside agent and network rows: class-aware filter dispatch (mergedListFilter), segment/columns/sort/bulk-bar generalization (DeviceList/DevicesPage/deviceClassFilter/DeviceClassSegment), a new ManualAssetModal for add/edit/link/unlink (MFA_REQUIRED handled via runAction, matching the API's requireMfa() contract on every mutator), a split Add-asset menu with a slot for #5228's network-asset entry point, a Playwright e2e spec, and a docs section explaining the discovery-vs-inventory split. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013peXzPJo3GG3kSGvscjWBU
Deploying breeze with
|
| Latest commit: |
7e0b493
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://04bd26a2.breeze-9te.pages.dev |
| Branch Preview URL: | https://feature-4622-manual-asset-en-6mk4.breeze-9te.pages.dev |
…silent failures, tests - DeviceCard.tsx (grid view) never got the manual-class treatment: a manual asset fell into the agent branch and got the full agent kebab (Terminal/Run Script/Reboot/Decommission/Permanent Delete) wired to real onAction handlers, plus an unconditional metrics-history fetch on every card mount — the same #4014 failure class already fixed for network rows. Now mirrors DeviceList's manual row Actions cell (Edit + Delete only) and skips the metrics fetch. - Widened DevicesPage.handleDeviceAction's network-only invariant guard to also refuse non-delete-manual actions on a manual row, so a future caller reopening this class of bug is caught at the funnel, not just by each UI surface hiding the actions. - ManualAssetModal.tsx: runBulkDeleteManual gained a missing outer catch (every sibling bulk-mutation function has one); handleSubmit/ handleLink/handleUnlink no longer risk mislabeling a post-success onSaved/onClose failure as a mutation failure; the contact-fetch best-effort catch and the per-item delete-manual catch now log instead of failing silently. - Added ManualAssetModal.test.tsx (name required, site defaults to the org's only site, duplicate-serial warning is non-blocking, link control is on the primary surface) and DeviceList.test.tsx coverage for bulk-delete-manual gating and the manual row Actions cell — gaps flagged by the PR's test-coverage review. - New devicesPage.toasts.agentOnlyActionManual locale key (8 locales). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013peXzPJo3GG3kSGvscjWBU
|
Review run: Findings: 2 critical + 4 silent-failure + 1 test-coverage report → all addressed in 324e714; 0 outstanding.
Tests: full Status: review-clean, awaiting maintainer merge. |
#5213's manual network assets W02 (#5258) and W03 (#5260) landed while this wave was open; both extend the same Devices surfaces. Resolution keeps both features in full: - DevicesPage: ONE Add control. main's inline split menu is folded into this wave's shared AddAssetMenu component, now three items (Install agent…, Add asset manually…, Add network asset…) in both the header and empty-state instances. Testids follow main's names (devices-page-add-menu*), so #5260's e2e spec is unchanged; this wave's Page Object was renamed to match. Side benefit: main's two instances shared one addMenuOpen flag (they opened together); AddAssetMenu holds per-instance state. - Locale catalogs (8): structural 3-way JSON union, plus devicesPage.addAsset / installAgent / addManualAssetAction folded into devicesPage.addMenu.* so the two features share one namespace. - DeviceList / columnVisibility: serial + assetTag + location (this wave) AND source (#5213) all kept. - bulkActionGating: took main's dedicated deviceActions.unavailable.unknown string over this wave's notOnline reuse — a manual asset was never online, so "not online" is the less honest label. - DeviceList.statusSortRank: kept this wave's ordering (decommissioned 6, unknown 7) — "no probe has ever run" is a different axis, not a state worse than offline. - no-silent-mutations: both handlers registered, count 123 -> 125. - translationCoverage: baselines recomputed as base + both deltas. Verified: apps/web 828 files / 9336 tests pass; apps/web and apps/api tsc clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcUieLXQuupVjBUSZUn5j
Merge review finding. #5213's devicesPage.hiddenNetworkNotice{,_one,_other} were superseded by this wave's more general devicesPage.hiddenNonAgentNotice (which covers hidden manual assets as well as hidden network devices). The locale union kept both; the network-only variants have zero references in apps/web/src or e2e-tests, and neither translationCoverage.test.ts nor keyUsage.test.ts flags unused keys, so they would have sat dead in all 8 catalogs indefinitely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcUieLXQuupVjBUSZUn5j
7e0b493 to
90333d7
Compare
|
Merge of origin/main after #5258/#5260 landed — resolution summary:
One Add control, not two. main shipped its own inline split menu; this wave had already extracted Locale key consolidation. This wave's flat Two genuine semantic clashes, resolved on the merits rather than by taking a side:
Verification
E2E (worktree stack, torn down after)
🤖 Generated with Claude Code |
…ession test (#5265) W04 of #4622 (#5261) added fetchAllManualAssets to DevicesPage's Promise.all; the harness mocked the module without it, so the page fell into the error branch and the device list never rendered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pc21knHQGa6fCM7UA9YtKX
Summary
Wave W04 of #4622 (manual asset entry) — the web-side third class of the unified Devices list, closing #5227. Depends on W01/W02 (
manual_assetstable +apps/api/src/routes/devices/manual.ts), already onmain.List integration (mergedListFilter / deviceClassFilter / DeviceClassSegment / DeviceList / DevicesPage)
DeviceClass→"agent" | "network" | "manual";DeviceStatusgains"unknown"(a manual asset has no reachability — renders an explicit Unknown chip, never "Offline").mergedListFilter.ts:manualFieldValuedispatch besidenetworkFieldValue;summarizeHiddenNetworkDevices→summarizeHiddenNonAgentDevices, now class-aware so the hidden-rows notice says "N manual assets hidden" instead of defaulting every non-agent row to "network devices"; search now matches serial number and asset tag; VPN facet excludes manual rows same as network.Packageicon); segment/notice block is gated onENABLE_NETWORK_DEVICES_IN_LIST || manual-rows-present— the Manual segment is independent of that flag, per spec, since the manual arm carries no feature flag of its own.serial(manual now, agent once the API sendshardware.serialNumber— plumbing is in place, no API change in this PR),assetTag,location(manual-only). Every two-branchdeviceClass === "network"ternary that would have silently folded manual into the wrong branch (sortValue, column availability, row actions) was swept to a three-way check.bulk-delete-manualaction gated on a manual-only allowlist (mirroring the existing agent-only allowlist), with "N of M eligible" reporting on a mixed selection. Row-level Edit/Delete replace the network arm's View for a manual row (no detail page in v1, per spec).requireMfa()contract — handled viarunAction'sfriendlymapper, matching the ArchiveOrgModal/MergeOrgModal MFA_REQUIRED precedent.ManualAssetModal.tsx(new)Add/edit modal: Org (fixed from page context or a fleet-view picker), Site (required, defaults to the org's only site), Name, Asset type (reuses the
discovered_asset_type/DeviceRoleenum + icons), Manufacturer, Model, Serial, Asset tag, Location, Assigned contact (client-filtered picker over org contacts — the route has no server-side search), Tags, Notes. The duplicate-serial warning (DUPLICATE_SERIAL) renders non-blockingly and does not prevent submission. The link control lives on the modal's primary surface, not a tab — #3451 records burying it in a tab as today's mistake to not repeat. Every mutation is wrapped inrunAction;ManualAssetModal.tsxis added tono-silent-mutations.test.ts's enforced target set (not the allowlist).Add-button composition with #5228
#5228(manual network assets,AddNetworkAssetModal) is not yet onmain. Per the wave brief, I built the split control (AddAssetMenu) with the manual item now and left an explicit, tested slot (a dropdown menu, not a bare button) for #5228's "Add network asset…" item — see the comment inAddAssetMenuinDevicesPage.tsx. Whoever lands #5228 adds one more<button>inside the existing menu.E2E + docs
e2e-tests/tests/manual-assets.spec.ts+ a newe2e-tests/pages/DevicesPage.tsPOM: add → see under the Manual segment → edit → delete,data-testidonly. Verified green against a realwt-stackstack (see Testing below) — not just written, actually run.apps/docs/src/content/docs/features/devices.mdx: new "Manual Assets" section explaining the discovery-vs-inventory rule (has a network identity → discovered asset / [API][UI] Manually add a network asset — hand-entered discovered_assets rows (source='manual') with full network-device parity (monitors, alerts, SNMP, tunnels, topology), incl. website/URL targets #5213; doesn't → manual asset), the add/edit/link/delete flow, and updates to the columns and class-facet lists.Testing
apps/web: full suite green — 824 files / 9288 tests passed (npx vitest run).npx tsc --noEmit -p apps/web— clean.npx tsc --noEmit -p apps/api— clean (confirms no shared-type break from theDevice/DeviceStatus/DeviceClasschanges).no-silent-mutations.test.ts— passes withManualAssetModal.tsxin the enforced set (bumped the frozen file-count assertion, not the allowlist).localeParity.test.ts/translationCoverage.test.ts— all 8 locales carry the new keys; the per-locale duplicate-baseline bumps are all genuine cognates/loanwords (Manual/Site/Notes/Tags/Model/Name), each commented in place.pnpm wt-stack up, rantests/manual-assets.spec.tsagainst it — 1 passed. (Needed local-only stack fixes unrelated to this feature to get a working dev.env:FORCE_HTTPS=false,ENABLE_2FA=false,REMOTE_ACCESS_ADMISSION_MODE/REMOTE_WS_AUTH_MODEset,MFA_FORCE_FOR_PARTNER_ADMIN=false— none of that is part of this PR's diff.) Tore the stack down after.No API changes, no migration — this wave is web-only, consuming the
GET/POST/PATCH/DELETE /devices/manualand/devices/manual/:id/linkcontract W02 already shipped.Follow-ups to file (per the plan's final section)
manual_asset_sourceenum already ships with'import'andcreateManualAssetSchemais array-friendly; needs preview/dry-run, per-row error reporting, and a stable idempotency key.custom_field_definitionswith anentity_typeand add amanual_asset_custom_field_valuestable; the two couplings to solve aredevice_custom_field_values's composite FK todevicesand itsdevices.custom_fieldsprojection trigger. A jsonbattributesbag is disqualified (lands inexcludedOpen, vanishes from tenant export).idand class discriminator on every DTO so Unified device list — per-asset detail pages and inline approval #1424 isn't blocked.Also worth noting, not filed as issues: warranty alerting stays device-only; manual assets are deliberately absent from
billableDeviceConds(not billable); the three-arm client-side merge inDevicesPage.tsx(fetchAllDevices+fetchAllNetworkDevices+fetchAllManualAssets, all concatenated and filtered client-side) remains #1424's debt to pay down, not deepened by this wave.Closes #5227
🤖 Generated with Claude Code
https://claude.ai/code/session_013peXzPJo3GG3kSGvscjWBU