Repository navigation
Customer portal networking visibility: a read-only network surface over existing discovery data #5438
fabicarvano
started this conversation in
Ideas
Replies: 1 comment
|
Greenlit. Networking is in scope as a new wave of the portal-visibility lineage, and the deferral boundary you drew (structural read-model now, real-fleet data fidelity later) is fine. I checked the premises: Conditions, all of which you already proposed, restated so they're the review bar:
Sequencing: one stacked PR per step as you outlined, each green on its own head before the next opens. Open step 1 whenever you're ready; I'll review it on the same cadence as your i18n work. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Most customers do not want to run their own network, and this does not ask them to. What they want is a small, honest window into it: which of their assets are being monitored, whether things are up or down right now, and a few operational signals they can glance at during the day without opening a ticket. This proposal is about that window and nothing more. It gives the customer read-only visibility into the network operation the MSP already runs on their behalf, with no control surface and no actions. The operating stays with the MSP, the portal only reflects its current state.
Why
The customer portal visibility epic (#4562) shipped waves W01 to W10 and gave end customers a real "log in and see it" surface for security, devices, backups, patches, tickets and reports. Networking was left out of that scope.
The data is already there and already org-scoped. The staff web app surfaces network discovery, SNMP health and monitor state today, and the schema has dedicated modules for all of it: discovered_assets (schema/discovery.ts), snmp_devices and snmp_metrics (schema/snmp.ts), and network_monitors (schema/monitors.ts), alongside devices. What is missing is a portal-scoped read path. A customer can see devices online or offline in apps/portal, but cannot see their network picture (discovered assets, SNMP-derived health, active monitors), even though every row needed to render that already exists behind the org boundary.
In the language of the #4562 audit, this is a "surface-only" gap: the data exists, there is no customer page and no portal API for it. This proposal closes that gap the same way the rest of the portal was built, as a read-model over existing data, with no new collection and no agent changes.
Proposed solution
Add one more wave to the portal-visibility lineage, reusing the architecture from W02 (#4570) through W07 (#4575). Read-only, org-scoped, fail-closed by default. Nothing here touches discovery, the agent, or how data is written. It only reads and shapes what is already stored.
A networkVisibility aggregator that reads discovered_assets, network_monitors and snmp_devices through the existing org-scoped system-context pattern from #4570. All queries filter on orgId, which is the tenant column on every one of these tables.
ts
// apps/api/src/services/portal/networkVisibility.ts
import { discoveredAssets, networkMonitors, snmpDevices } from '../../db/schema';
import { and, count, eq } from 'drizzle-orm';
export async function getNetworkOverview(orgId: string): Promise {
const [{ total }] = await db
.select({ total: count() })
.from(discoveredAssets)
.where(eq(discoveredAssets.orgId, orgId));
if (total === 0) {
return {
dataStatus: 'no_data',
discoveredAssets: null,
onlineAssets: null,
monitorsDown: null,
snmpDevicesPolling: null,
};
}
const [{ online }] = await db
.select({ online: count() })
.from(discoveredAssets)
.where(and(eq(discoveredAssets.orgId, orgId), eq(discoveredAssets.isOnline, true)));
// networkMonitors and snmpDevices are counted the same way, all filtered by orgId
return {
dataStatus: 'ok',
discoveredAssets: total,
onlineAssets: online,
monitorsDown,
snmpDevicesPolling,
};
}
2. DTO as a discriminated union (never-invent-zeros)
Following the invariant from #4629: a missing source produces null values plus a non-ok status, encoded in the type rather than described in prose, and reusing the dataStatus field name that the overview and list DTOs already use.
ts
// packages/shared/src/types/portalVisibility.ts
export type NetworkOverviewDto =
| {
dataStatus: 'ok';
discoveredAssets: number;
onlineAssets: number;
monitorsDown: number;
snmpDevicesPolling: number;
}
| {
dataStatus: 'no_data' | 'not_enabled';
discoveredAssets: null;
onlineAssets: null;
monitorsDown: null;
snmpDevicesPolling: null;
};
This makes the empty state unrepresentable as "ok with zeros", which is exactly the class of trust hazard W01 (#4569) was created to remove.
Add a portal_branding flag (for example enableNetworkVisibility), default off, and gate both the route and the portal nav item exactly like the five flags introduced in W03 (#4571). If the flag is off, the route returns not_enabled and the page is not reachable, so there is no accidental exposure.
A read route such as GET /portal/network, org-scoped and behind the gate, plus the apps/portal page that consumes the DTO. This mirrors the dashboard API and page work in W04 (#4572) and the security page in W05 (#4573).
Per-asset columns (hostname, IP, type, online state, last seen), a CSV export, and an e2e spec, mirroring the devices enrichment and export in W07 (#4575) and the reports tab e2e in W10 (#4578).
Each step is a small stacked PR, each green on the CONTRIBUTING.md gate before the next, so review stays incremental.
Test strategy without live assets
Because this is a read-model over existing tables, it is fully testable against a fixture database (Postgres via docker compose), the same way the W02 read-model foundation was validated with an RLS integration test. No real network gear is required at any point.
Synthetic network data is created the way #5213 established it: a manual network asset is a discovered_assets row with source='manual', and monitors or SNMP devices attach to it through assetId, which references discovered_assets.id on both network_monitors and snmp_devices. The test covers three cases:
ts
// apps/api/src/services/portal/networkVisibility.integration.test.ts
it('scopes network data by org and returns no_data for an empty org', async () => {
const orgA = await seedOrg();
const orgB = await seedOrg();
// a manual network asset is a discovered_assets row (the #5213 path)
const asset = await insertDiscoveredAsset(orgA, {
hostname: 'core-sw-01',
ipAddress: '10.0.0.1',
isOnline: true,
});
await insertNetworkMonitor(orgA, { assetId: asset.id, isActive: true });
const a = await withOrgContext(orgA, () => getNetworkOverview(orgA));
expect(a.dataStatus).toBe('ok');
expect(a.onlineAssets).toBe(1);
// RLS isolation plus the no-data path in one assertion
const b = await withOrgContext(orgB, () => getNetworkOverview(orgB));
expect(b.dataStatus).toBe('no_data');
expect(b.discoveredAssets).toBeNull();
});
The empty-org case is the "no assets" scenario, so the absence of a live fleet is not a blocker. It is a required test path, and the never-invent-zeros union is what it exercises.
What this defers on purpose
Real-fleet data-fidelity validation. Confirming that live discovery, SNMP and UniFi populate these fields exactly as the fixtures assume (real-world quirks such as the UniFi snake_case decode issue in #5087, or the synthetic-topology concern raised in #1325) needs an environment with populated assets. That is validation of data, not of code, and it is best done by a maintainer or user who runs a live fleet.
To keep the handoff clean rather than a silent gap, the fixtures are shaped directly from the real schema (schema/discovery.ts, schema/snmp.ts, schema/monitors.ts), so the contract is correct and only field content needs confirmation against live data. This mirrors how the i18n Phase 3 work merged: pt-BR reviewed directly, the other locales machine-drafted and marked pending native review.
Alternatives considered
Fold the network view into the existing portal Devices page. Rejected because discovered assets, SNMP interfaces and monitor state have a different shape than the per-device protection table, and would overload the Devices DTO.
Expose the staff network endpoints to the portal directly. Rejected because those routes are not scoped for portal auth and would bypass the fail-closed visibility gate.
Component and scope
Component: API Server plus Web Dashboard (portal). Scope: Large (a new portal surface), but built entirely over existing data, with no collection or agent changes.
Ask
Before I open the stacked PRs, I would like a maintainer greenlight on two things: that networking is in scope as a new wave of the portal-visibility lineage, and that the deferral boundary above (structural layer now, real-fleet data validation later) is acceptable. With that confirmation I can start on step 1.
All reactions