Problem
An MSP cannot hand-add a network asset that a scan does not reach (a printer on an unscanned VLAN, a VPN appliance at a remote site, a customer website or SaaS endpoint) and have it behave like every other network device: appear in the unified Devices list, take monitors on the poll cycle, raise alerts, be SNMP-polled, be tunnelled to, and sit in the topology.
Split out of #4622 on 2026-09-07. #4622 stays the simpler non-network inventory record (manual_assets, option B in its spec). The rule between the two: if it has a network identity, it is a network asset and lives here; if it does not, it is a manual asset under #4622.
Current state (verified on main 2026-09-07)
discovered_assets has no create route at all: only approve/dismiss/link/edit/delete (apps/api/src/routes/discovery.ts), and its only writers are the scan worker and UniFi sync. ip_address is inet NOT NULL and half of discovered_assets_org_ip_unique (org_id, ip_address) (apps/api/src/db/schema/discovery.ts).
- Standalone monitors already exist:
POST /monitors takes an optional assetId and a free-text target for icmp_ping | tcp_port | http_check | dns_check (apps/api/src/routes/monitors.ts:228-231), with CreateMonitorForm and the Monitoring page. So an HTTP check on a website works today, but it is a monitor with no asset behind it and never appears in the Devices list.
- Everything "a network device has" is keyed on a
discovered_assets row: jobs/monitorWorker.ts (probe agent chosen by the asset's site), routes/monitors.ts, routes/snmp.ts, routes/monitoring.ts, routes/tunnels.ts, routes/partnerApi/inventory.ts, routes/devices/network.ts, services/networkBaseline.ts, services/aiToolsMonitoring.ts, services/discoveredAssetClassification.ts, jobs/reconcileTopology.ts. Parity therefore means: a manual network asset is a discovered_assets row.
Proposed behavior
POST /devices/network (or /discovery/assets) + "Add network asset" from the Devices list and the Monitoring page: org + site (required), name/label, asset type, IP and/or hostname, optional URL for website-type rows, manufacturer/model, notes, tags. Born approval_status = 'approved', type_source = 'manual'.
- New columns on
discovered_assets: source enum (scan | unifi | manual, NOT NULL, backfilled from discovery_methods/UniFi linkage), url (nullable). Add website (and possibly service) to discovered_asset_type.
- Decision (recommendation inline): allow
ip_address NULL for manual rows so websites and DNS-only targets can be assets; make discovered_assets_org_ip_unique partial on ip_address IS NOT NULL. Alternative: keep IP NOT NULL and resolve the hostname at create time — rejected because a website's IP is not stable and the row would go stale.
- Guards, each with a test: manual rows never enter the pending-approval queue; the scan upsert on
(org_id, ip_address) already respects type_source = 'manual' for type — extend the same guard to label; a scan that finds a manual row's IP updates it in place (one identity, no duplicate row); disappeared-asset events key on last_seen_at IS NOT NULL so a never-scanned manual IP does not alert on the first sweep of its subnet; host(ip_address) search and any ORDER BY ip_address are NULL-safe.
- Export policy: classify every new column (
source, url → included); no new cascade registration — the table is already in every list.
Acceptance criteria
Related
#4622 (manual non-network assets), #1424 (per-asset detail pages), #1322 (network arm), #3451 (link/unlink).
Problem
An MSP cannot hand-add a network asset that a scan does not reach (a printer on an unscanned VLAN, a VPN appliance at a remote site, a customer website or SaaS endpoint) and have it behave like every other network device: appear in the unified Devices list, take monitors on the poll cycle, raise alerts, be SNMP-polled, be tunnelled to, and sit in the topology.
Split out of #4622 on 2026-09-07. #4622 stays the simpler non-network inventory record (
manual_assets, option B in its spec). The rule between the two: if it has a network identity, it is a network asset and lives here; if it does not, it is a manual asset under #4622.Current state (verified on
main2026-09-07)discovered_assetshas no create route at all: only approve/dismiss/link/edit/delete (apps/api/src/routes/discovery.ts), and its only writers are the scan worker and UniFi sync.ip_addressisinet NOT NULLand half ofdiscovered_assets_org_ip_unique (org_id, ip_address)(apps/api/src/db/schema/discovery.ts).POST /monitorstakes an optionalassetIdand a free-texttargetforicmp_ping | tcp_port | http_check | dns_check(apps/api/src/routes/monitors.ts:228-231), withCreateMonitorFormand the Monitoring page. So an HTTP check on a website works today, but it is a monitor with no asset behind it and never appears in the Devices list.discovered_assetsrow:jobs/monitorWorker.ts(probe agent chosen by the asset's site),routes/monitors.ts,routes/snmp.ts,routes/monitoring.ts,routes/tunnels.ts,routes/partnerApi/inventory.ts,routes/devices/network.ts,services/networkBaseline.ts,services/aiToolsMonitoring.ts,services/discoveredAssetClassification.ts,jobs/reconcileTopology.ts. Parity therefore means: a manual network asset is adiscovered_assetsrow.Proposed behavior
POST /devices/network(or/discovery/assets) + "Add network asset" from the Devices list and the Monitoring page: org + site (required), name/label, asset type, IP and/or hostname, optional URL forwebsite-type rows, manufacturer/model, notes, tags. Bornapproval_status = 'approved',type_source = 'manual'.discovered_assets:sourceenum (scan | unifi | manual, NOT NULL, backfilled fromdiscovery_methods/UniFi linkage),url(nullable). Addwebsite(and possiblyservice) todiscovered_asset_type.ip_addressNULL for manual rows so websites and DNS-only targets can be assets; makediscovered_assets_org_ip_uniquepartial onip_address IS NOT NULL. Alternative: keep IP NOT NULL and resolve the hostname at create time — rejected because a website's IP is not stable and the row would go stale.(org_id, ip_address)already respectstype_source = 'manual'for type — extend the same guard tolabel; a scan that finds a manual row's IP updates it in place (one identity, no duplicate row); disappeared-asset events key onlast_seen_at IS NOT NULLso a never-scanned manual IP does not alert on the first sweep of its subnet;host(ip_address)search and anyORDER BY ip_addressare NULL-safe.source,url→included); no new cascade registration — the table is already in every list.Acceptance criteria
websiteasset with a URL and no IP; anhttp_checkmonitor attached to it shows status in the list.Related
#4622 (manual non-network assets), #1424 (per-asset detail pages), #1322 (network arm), #3451 (link/unlink).