NFWeb Developer Guide¤
This is the working guide for developers and coding agents changing NFWeb. Read
it before editing norfab/clients/nfweb. It describes the current implementation,
the boundaries that must remain true, and the shortest safe workflows for extending
the client.
NFWeb is NORFAB's generic local web client. Its built-in applications are runtime Monitoring and 3D Topology; neither defines the final scope of NFWeb.
Start Here¤
For a new development session:
- Read
AGENTS.mdfor repository-wide commands and conventions. - Read the NFWeb architecture decision for accepted product and security boundaries. When planning terminal work, also read the proposed browser-native terminal decision.
- Read the user-facing NFWeb overview, monitoring dashboard guide, and topology application guide.
- Inspect the current working tree before editing. NFWeb may be under active development, and uncommitted files belong to the developer.
- Treat the current source and tests as authoritative when this guide and the implementation differ. Update this guide when a deliberate architecture or contract change makes it stale.
Preserve These Boundaries¤
Every NFWeb change must preserve these rules unless a new architecture decision explicitly replaces them:
- NFWeb is a generic client and local application host. Monitoring and Topology remain namespaced applications.
nfcli --web-uiis the only NFWeb launcher. Do not add annfwebexecutable or another dedicated command-line utility.- NFWeb connects through the native Python
NFPClientto an existing broker. It does not start a broker or workers. - NFWeb does not depend on the FastAPI worker. Do not route browser requests through a FastAPI service just to reach the fabric.
- The browser API exposes focused application operations. It must not expose a
generic
service,task, andkwargsjob-execution endpoint. - The listener binds to
0.0.0.0by default and accepts reachable remote clients. It has no built-in authentication, authorization, or TLS. - Browser HTTP routes and WebSocket upgrades currently accept every origin.
- Production requires Python only. Node.js is a frontend development and release build tool.
- Frontend dependencies are pinned and bundled. Do not add runtime CDN assets.
- Use Mantine components and Tabler icons for general-purpose controls. Do not create bespoke buttons, selects, dropdowns, tabs, sliders, badges, or tooltips. Keep custom CSS focused on NFWeb layout and domain-specific visualization.
- Use ECharts through
echarts-for-reactfor Monitoring charts. Do not build a separate React chart wrapper or use Sigma.js/Graphology for time-series and gauges; those libraries solve graph-network visualization, not dashboard charts. - The topology application uses Vasturiano 3D visualization only. Do not add a 2D renderer or a 2D/3D mode.
- An empty device selection performs no topology collection.
- Operational failures remain visible. Do not fabricate topology, health, or metrics when a worker does not return data.
- Local application history is bounded. Topology defaults to a maximum three-hour retention window. Monitoring has the same maximum but keeps samples only in process memory and loses them on restart.
Repository Map¤
norfab/clients/nfweb/
|-- __init__.py
|-- application.py Shared NFWeb application-module protocol
|-- config.py Shared NFWeb configuration
|-- runtime.py nfcli lifecycle, module composition, shutdown
|-- server.py Shared Tornado host and browser security policy
|-- topology/
| |-- application.py Topology module lifecycle and route composition
| |-- config.py Topology application configuration
| |-- models.py Authoritative Python browser/storage contracts
| |-- layers.py NORFAB calls and layer adapters
| |-- collector.py Scheduling, caching, merge, publication
| |-- history.py Compressed SQLite snapshots and derived logs
| `-- web.py Topology HTTP, WebSocket, and broadcaster
|-- monitoring/
| |-- application.py Monitoring lifecycle and route composition
| |-- config.py Polling and in-memory retention configuration
| |-- models.py Presentation-oriented monitoring contracts
| |-- collector.py Existing-interface polling and bounded deque
| `-- web.py Monitoring HTTP, WebSocket, and broadcaster
|-- frontend/
| |-- package.json Pinned frontend tools and runtime dependencies
| |-- package-lock.json Reproducible dependency lock
| |-- vite.config.ts Development proxy and static build destination
| `-- src/
| |-- main.tsx React entry point
| |-- App.tsx Shared shell, view selection, topology composition
| |-- TopologyGraph.tsx Lazy-loaded Vasturiano/Three.js graph
| |-- api.ts Typed HTTP and WebSocket client
| |-- graphModel.ts Graph identities, styling, and curve helpers
| |-- types.ts Manual TypeScript mirror of Python contracts
| |-- components/ Mantine toolbar, navigation, inspector, timeline
| |-- monitoring/ ECharts runtime monitoring view
| `-- styles.css NFWeb layout and domain presentation
`-- static/ Generated production assets served by Tornado
tests/clients/nfweb/
|-- test_config.py Strict configuration and safety defaults
|-- test_collector.py Worker payloads, adapters, merge, failures, events
|-- test_history.py Retention, scope filtering, persistent logs
|-- test_monitoring.py Status merging and in-memory retention
|-- test_server.py Routes, request policy, static assets
`-- test_frontend_contracts.py Python/TypeScript fields and packaged assets
Do not edit norfab/clients/nfweb/static by hand. npm run build deletes and
regenerates that directory from frontend/src.
Understand the Runtime¤
norfab/utils/nfcli.py handles --web-ui and calls
norfab.clients.nfweb.runtime.serve().
The runtime performs this lifecycle:
nfcli --web-ui
-> load the normal NORFAB inventory
-> validate client.nfweb with NFWebConfig
-> create the native client named "nfweb"
-> open __norfab__/nfweb/nfweb.sqlite
-> compose Topology and Monitoring application modules
-> validate and create the Tornado application
-> listen on the configured host (0.0.0.0 by default)
-> start Monitoring polling and scoped Topology collection
-> optionally open the browser
-> on first Ctrl+C: stop HTTP, modules, SQLite, and native client
-> on second Ctrl+C: force process exit if graceful cleanup is stuck
The NorFab object is used to load inventory and create the client. NFWeb does not
call nf.start() and therefore does not start local topology processes.
Configuration Ownership¤
client.nfweb contains shared runtime configuration. Each application owns a
nested configuration model:
client:
nfweb:
host: 0.0.0.0
port: 9005
open_browser: true
footer:
message: "Managed by the Network Automation team"
fastapi_url: "http://127.0.0.1:8000/docs"
docs_url: "https://docs.norfablabs.com/"
github_url: "https://github.com/norfablabs/NORFAB"
monitoring:
collection_interval: 5
retention_minutes: 180
request_timeout: 10
topology:
devices: []
collection_interval: 30
inventory_refresh_interval: 300
retention_minutes: 180
request_timeout: 60
netbox_workers: any
nornir_workers: all
layers:
topology: true
lldp: true
bgp: true
interfaces: true
All configuration models use extra="forbid". Do not silently accept unknown
settings.
footer is shared shell configuration. Only these display-safe fields are
returned by /api/v1/config; application configuration and broker details must
not be exposed to the browser. A null footer URL hides its pictogram link.
Follow Topology Data Through the System¤
The topology data path is:
NetBox and Nornir workers
-> layer adapters produce LayerPatch values
-> TopologyCollector merges patches into TopologySnapshot
-> TopologyHistoryStore compresses and stores the snapshot
-> TopologySnapshotBroadcaster publishes it over WebSocket
-> React filters the snapshot and updates ForceGraph3D
Discovery Is Separate From Collection¤
Device discovery calls only Nornir get_nornir_hosts. NetBox-only devices are not
selectable. Discovery does not collect graph data; collection begins only after
the operator applies a non-empty selection or topology.devices supplies a
startup scope.
Changing the selected devices validates them against discovery, sorts and deduplicates them, clears all layer caches, and immediately performs a forced collection. Clearing the selection clears the active view and stops later periodic cycles from submitting work.
Worker Calls¤
| Purpose | Service and task | Important kwargs |
|---|---|---|
| Discover Nornir devices | nornir.get_nornir_hosts |
none |
| Seed selected devices | nornir.get_inventory |
FL=[...] |
| Intended topology | netbox.get_topology |
devices=[...] for the active scope |
| Observed adjacency | nornir.parse_ttp |
get="lldp_neighbors", FL=[...] |
| BGP sessions | nornir.parse_ttp |
get="bgp_neighbors", FL=[...] |
| Resolve BGP peer IPs | netbox.crud_read |
unresolved IP addresses only |
| Interface state | nornir.parse_ttp |
get="interfaces_status", FL=[...] |
Use these exact TTP getter names. In particular, interface collection uses
interfaces_status.
_submit_job() always uses the native client's asynchronous submit_job() API and
retains events from the returned future. Worker errors become
TopologyCollectionError values; usable results from other workers remain in a
partial snapshot.
Scheduling and Caching¤
TopologyCollector submits native NORFAB jobs with submit_job and asynchronously
waits for their futures without blocking Tornado's event loop. An asyncio.Lock
prevents overlapping topology collection cycles.
Adapters run in dependency order:
- scoped Nornir inventory;
- scoped NetBox topology;
- LLDP;
- BGP;
- interface observations.
Inventory populates the IP-to-device map before BGP resolution. Each adapter has
an independent cache timestamp. Normal periodic collection reuses a fresh cache;
the Refresh action calls collect(force=True) and bypasses every layer cache.
Do not add another independent browser-owned polling loop. One collector is shared by all browser tabs.
Keep Graph Contracts Stable¤
The Pydantic models in topology/models.py are the authoritative contracts for
storage and browser responses. frontend/src/types.ts is a manual mirror. A field
change is incomplete until both sides and their tests are updated.
Node Rules¤
TopologyNode.idis the stable merge and ForceGraph identifier.- A node can belong to multiple layers.
- Duplicate nodes merge by ID. Health becomes the worst reported health, layers are unioned, and stronger device metadata replaces placeholder metadata.
- Nodes have no inferred kind. Unresolved BGP addresses use the IP as their ID.
- Every node carries a sorted union of
netbox,nornir, andlive-lldporigins. - Do not use labels as IDs unless the worker contract guarantees the label is the canonical device identity.
Link Rules¤
TopologyLink.idis the merge identity.- Physical link IDs include normalized device and interface endpoints. BGP link IDs use normalized session addresses.
_link_id()sorts its twodevice:interfaceendpoints. LLDP reverse advertisements with exactly matching names therefore produce one link.sourceandtargetare required by ForceGraph but do not automatically mean a relationship is directional. LLDP and physical cables are undirected.- Exact normalization matters.
Ethernet1andEth1, or a short hostname and an FQDN, create different endpoint IDs. Add an explicit normalization policy and tests before trying to collapse those variants. - NetBox, cached Nornir connections, and live LLDP relationships with matching endpoints merge into one topology link and union their origins. BGP remains a protocol layer.
- Multiple real connections between the same devices remain distinct in the snapshot and inspector because interface names are part of their IDs.
- The interfaces adapter creates observations, not standalone graph links. During
merge, an observation decorates a matching link endpoint with health,
attributes, and metrics. The frontend therefore excludes
interfacesfrom the graph-layer visibility controls.
For rendering, the frontend bundles links by unordered node pair and layer. All LLDP records between two nodes become one LLDP link, all NetBox records become one NetBox link, and all BGP records become one BGP link. Different layers receive parallel curves and remain independently selectable. Each rendered bundle keeps its raw member records for inspection, reports their count, uses their worst health, and uses their highest utilization for link width. Link colour represents the discovery layer. Node colour and the inspector carry health.
Snapshot Rules¤
Snapshot status is derived after merging:
complete: graph data and no errors;partial: graph data plus one or more errors;failed: errors and no graph data;empty: neither graph data nor errors.
Unknown values must remain unknown. Do not turn a missing rate, utilization, or state into zero or healthy.
Work With History and Logs¤
The generic database is:
<inventory-base>/__norfab__/nfweb/nfweb.sqlite
Topology owns the topology_snapshots table. Each row stores a timestamp and a
zlib-compressed JSON representation of the complete TopologySnapshot.
History and logs are filtered by an exact, sorted device list. Logs are not stored
in a second table. TopologyHistoryStore.logs() derives terminal entries from the
events and errors embedded in retained snapshots and returns at most 300 entries.
Cleanup runs when the store opens and after every insert. The configuration model prevents retention beyond 180 minutes.
For a future application, add application-owned tables inside the same generic database. Do not name the database after topology, and do not reuse the topology snapshot table for an unrelated domain model.
Follow Monitoring Data Through the System¤
Monitoring is deliberately smaller than Topology and does not use the database:
broker get_stats + worker get_stats + local client get_stats
-> MonitoringCollector creates one MonitoringSnapshot
-> bounded deque retains at most the configured three-hour window
-> MonitoringBroadcaster publishes the sample over WebSocket
-> ReactECharts redraws gauges, trends, and worker comparisons
Worker selection separately calls the existing worker job_list task through a
namespaced NFWeb endpoint. NFWeb aggregates its bounded result for the horizontal
worker database card; no worker implementation or worker database schema is
changed.
The collector also reads process resources, message counters, and aggregate job
statistics directly from the local NFWeb process and native client. Collection runs in asyncio.to_thread() so
the synchronous native calls do not block Tornado. One asyncio.Lock prevents
overlapping cycles. Keep the polling sequential and direct unless measurements
show a real need for more scheduling complexity.
MonitoringComponent and MonitoringSnapshot in monitoring/models.py are the
authoritative browser contracts. frontend/src/types.ts mirrors them. Missing
worker watchdog data must stay null; the broker registration row remains useful
and should not be discarded or filled with invented values. Add a snapshot error
that names every worker with an absent, failed, or incomplete resource response;
the frontend displays these errors in the Fabric observability toolbar.
History is a bounded deque, pruned both by sample count and exact timestamp. It
must remain non-persistent: do not add a monitoring table, journal, log replay, or
restart restoration. The dashboard shows message and keepalive counters available
from existing interfaces; it does not inspect or retain protocol payloads.
The browser merges HTTP history, interval reads, and WebSocket samples by
collected_at, deduplicates them, and never replaces the latest sample with an
older response. WebSocket URLs follow the page scheme (ws:// for HTTP and
wss:// for HTTPS).
Use the Browser API Deliberately¤
Current routes are:
| Method | Route | Purpose |
|---|---|---|
GET |
/api/v1/health |
Shared runtime and application health |
GET |
/api/v1/config |
Display-safe shared footer configuration |
GET |
/api/v1/monitoring/snapshot |
Latest in-memory monitoring sample |
GET |
/api/v1/monitoring/history |
Retained in-memory samples |
POST |
/api/v1/monitoring/refresh |
Request an immediate sample |
GET |
/api/v1/monitoring/workers/{name}/database |
Summarize the selected worker's recent job_list result |
| WebSocket | /api/v1/monitoring/stream |
Latest and newly completed samples |
GET |
/api/v1/topology/devices |
Nornir device discovery and active scope |
POST |
/api/v1/topology/selection |
Apply a scope and collect when non-empty |
POST |
/api/v1/topology/refresh |
Force a cache-bypassing collection |
GET |
/api/v1/topology/history |
Timestamped snapshot entries for the active scope |
GET |
/api/v1/topology/snapshots/{id} |
One stored snapshot |
GET |
/api/v1/topology/logs |
Up to 300 retained terminal entries |
| WebSocket | /api/v1/topology/stream |
Latest and newly completed snapshots |
POST routes and WebSocket upgrades do not validate the browser origin. Static assets receive a restrictive content security policy.
Add new endpoints under an application namespace. Add the Python handler, route, frontend method, TypeScript response type, and server test in one change.
Understand the Current Frontend¤
The current frontend has one shared component system:
main.tsxmounts one React application in strict mode insideMantineProviderand behind a fatal error boundary.api.tsowns JSON requests and WebSocket reconnection.types.tsmirrors the Pydantic browser contracts.App.tsxowns the shared shell and hash-selected application view, and composes the existing Topology state, graph, inspector, timeline, and toolbar.monitoring/MonitoringView.tsxowns Monitoring history, component selection, refresh state, and directReactEChartschart options.TopologyGraph.tsxisolates the imperative Vasturiano/Three.js integration and is loaded only when the topology view is rendered.graphModel.tscontains renderer-independent graph identities and calculations.- Mantine supplies common controls and interaction states;
styles.cssowns the shell grid and domain-specific graph, inspector, and terminal presentation. Do not target Mantine's internal class names from this stylesheet.
Frontend Stack¤
frontend/package.json and frontend/package-lock.json are the source of truth
for frontend versions. The current stack is:
| Concern | Technology | Responsibility |
|---|---|---|
| UI runtime | React and React DOM 19.2.8 |
Component rendering, state, and browser mounting |
| Language | TypeScript 7.0.2 |
Static checking for components, graph models, and API contracts |
| Component system | Mantine Core and Hooks 9.5.2 |
Standard controls, layout primitives, overlays, themes, and interaction hooks |
| Icons | Tabler Icons React 3.46.0 |
Consistent application and footer pictograms |
| Topology renderer | React Force Graph 3D 1.29.1 |
React integration for the interactive 3D topology |
| 3D engine | Three.js 0.185.1 |
WebGL rendering and graph scene objects |
| Monitoring charts | ECharts 6.1.0 and ECharts for React 3.0.6 |
Gauges, time series, data zoom, and worker comparisons |
| Build tooling | Vite 8.2.2 with the React plugin 6.1.0 |
Development server, module bundling, and hashed production assets |
| Unit tests | Vitest 4.1.11 |
Component, graph-model, and regression tests without a browser |
| Browser tests | Playwright 1.62.1 |
Chromium end-to-end tests for the rendered application and its controls |
The browser entry point imports Mantine's base stylesheet before NFWeb's
styles.css. Use Mantine components and their public props for general-purpose
controls. Use Tabler icons for application actions. Reserve custom CSS and React
components for NFWeb-specific layout, topology, inspector, and terminal behavior.
Vite writes the production bundle to norfab/clients/nfweb/static. The Python
server serves those committed assets directly, so production does not require
Node.js or a CDN. The topology renderer and Monitoring view are lazy-loaded so
their visualization libraries remain outside the initial application bundle.
The left navigation follows Mantine UI's nested-navbar pattern for generic NFWeb
applications and remains a fixed-width shell column. Topology remains the default;
the URL hash selects Monitoring. Topology controls, including
the snapshot selector, return-to-live action, and trailing stream-status badge,
belong in one non-wrapping, horizontally scrollable application toolbar, not the
navigation panel. Link controls use compact Mantine menus. Topology is a union
multi-select for NetBox, Nornir connections, LLDP, and circuits. Protocols
contains BGP and is the extension point for later protocol layers. Stats is a
single-value choice between Off, Traffic, Errors, and Flaps. The remaining
desktop content width is split 80% for the topology stage and 20% for the
inspector, and the inspector spans into the top-right corner above its content.
The graph-control group includes a bloom toggle implemented with Three.js
UnrealBloomPass. Search text is a draft until the user presses Enter or clicks
the magnifying-glass button. Applying a search keeps every node and link allowed
by the layer and health filters visible, then enlarges and brightens only directly
matching nodes or links. Node-name matches do not spread to adjacent links or
neighbouring nodes. Clicking the active
magnifying glass disables the highlight without discarding the typed query.
Every inspector tab uses the same Mantine searchable, sortable table composition;
add domain columns and row adapters instead of another details layout. A shared
footer owns the configured message and external resource pictograms. Overview and
Admin are intentionally empty placeholders.
Monitoring uses Mantine for cards, alerts, selects, buttons, and badges.
It passes ECharts options directly to ReactECharts; avoid a local abstraction
until more than one monitoring view has concrete shared requirements. Aligned
worker CPU and memory comparisons use the same worker order. The bottom worker
selection drives separate CPU and memory time series, while the adjacent job bar
chart stacks positive FAILED, STARTED, COMPLETED, and STALE count changes
between retained native-client database samples. It must not infer exact job
transitions because the aggregate database statistics do not contain a transition
journal.
The selected worker also drives a full-width database status card below those
charts. Its request is independent of periodic snapshot collection and failures
remain local to the card.
Comparison charts rank workers by CPU percentage plus memory relative to the
largest current worker and show ten rows at a time. React owns one shared worker
window for both charts so vertical scrolling stays synchronized; each new sample
resets that window to the newly ranked top ten instead of retaining stale,
chart-local ECharts zoom state. The dashboard refresh selector
reads the latest shared sample every 5, 10, 30, or 60 seconds without changing the
server's configured collection interval or starting another collection. Manual
refresh requests that overlap the server collector are coalesced onto the latest
completed sample and must not return a routine 409 warning.
The fabric consumption row derives stacked broker and per-worker CPU and memory
series entirely from retained MonitoringSnapshot history. React owns their
shared time-axis zoom so chart-local ECharts state cannot drift; it must not issue
additional worker requests. Historical series retain workers that disappear from
the latest sample and render a gap after their final observation.
Live and Historical State¤
The frontend retains the newest WebSocket snapshot separately from the frame being displayed. In live mode, new frames update the graph. Selecting history from the top bar's timestamp dropdown disables live display without stopping collection. Return to live selects the newest known snapshot.
Collection logs are merged by stable entry ID and bounded to 300 lines in browser
memory. The initial /logs request restores retained events after page reload.
Vasturiano and Three.js Rules¤
react-force-graph-3d wraps an imperative Three.js graph. Follow these rules:
- Pass graph data through the React
graphDataprop. - The React ref exposes animation, camera, force, and refresh methods, but it does
not expose the underlying library's
graphData()getter. CallinggraphRef.current.graphData()causes a runtimeTypeError. - Vasturiano mutates node objects with
x,y,z, velocity, and resolved link endpoints. UseendpointId()whenever a link endpoint may be either a string or a mutated node object. renderedGraphDataretains the actual object passed to the graph so callbacks can read mutated coordinates safely.positionsstores coordinates by stable node ID before a live snapshot is replaced.- Pausing layout pins nodes through
fx,fy, andfz. Starting layout removes those fields and reheats the D3 force simulation. - Layout distance changes the link force's
distanceparameter. It is not camera zoom. - Pausing the view stops rendering animation. It is separate from pausing the force layout.
- Node labels are Three.js sprites added with
nodeThreeObjectExtend; they do not replace the selectable node sphere. - Do not recreate the graph solely to update controls or a snapshot. Preserve the camera, selection, and saved coordinates.
LLDP and physical cabling remain undirected topology relationships. When Traffic statistics are selected, a link with interface telemetry is rendered as two shallow curved visual lanes around that one relationship. The overlay applies to physical topology links, leaving BGP peerings on their own layer paths. Forward traffic uses source output or target input telemetry; reverse traffic uses target output or source input telemetry. Particle speed is bounded and proportional to the reported bit rate, with utilization as a fallback. Lane color changes at warning and critical utilization thresholds. The reverse visual lane has zero D3 link-force strength, so enabling traffic does not add another physical relationship to the layout. Errors uses input, output, and CRC counters. Flaps is green through 10 transitions, yellow through 100, and red above 100. Links without the selected statistics remain visible in subdued grey; unknown values are never invented. Node spheres are fully opaque and use the same green, yellow, red, and gray health palette shown in the States dropdown. Traffic retains a separate teal, amber, and pink scale.
Run a Development Environment¤
NFWeb needs an existing broker and the workers used by the selected applications. Start those processes separately, then run from the repository root:
poetry run nfcli --inventory tests/nf_tests_inventory/inventory.yaml --web-ui
NFWeb binds to 0.0.0.0:9005 by default and normally opens a URL containing the
machine's preferred LAN IP. Use open_browser: false while restarting the backend
frequently, or set host: 127.0.0.1 for loopback-only development.
For frontend dependencies and a production-style build:
cd norfab/clients/nfweb/frontend
npm ci
npm test
npx playwright install chromium
npm run test:e2e
npm run typecheck
npm run build
Playwright starts an isolated Vite server on 127.0.0.1:4173. Its checked-in
fixtures mock the browser API and application WebSockets, so browser tests do not
require a running broker or worker. Run npm run test:e2e:headed to observe the
same tests in a visible Chromium window.
The build output goes directly to ../static. A backend restart is required for
Python changes. A hard browser refresh loads a newly hashed frontend bundle after a
frontend build.
Vite development mode runs on 127.0.0.1:5173 and proxies /api to port 9005:
cd norfab/clients/nfweb/frontend
npm run dev
When changing live delivery, verify both HTTP requests and the relevant application WebSocket; do not assume that a working HTTP proxy proves WebSocket upgrades work.
Verify Changes¤
Run focused checks from the repository root:
poetry run pytest tests/clients/nfweb -q
For frontend changes:
cd norfab/clients/nfweb/frontend
npm test
npm run build
npm run build runs both TypeScript projects before Vite creates production
assets. The large WebGL renderer is emitted as a lazy chunk, keeping it out of the
initial application bundle. Set NFWEB_SOURCEMAPS=true to emit hidden source maps
for a diagnostic build; production builds omit them by default.
Before publishing a Python package, run the repository packaging workflow:
poetry run inv package-build
This compiles the NFWeb frontend, removes the generated frontend/node_modules
directory so Poetry does not spend time scanning it, and then builds the wheel and
source distribution. It runs npm ci first when dependencies are not installed.
Use the ordinary npm run build during frontend development because it keeps the
dependencies available for subsequent builds and tests.
For documentation changes:
poetry run inv docs-build
Also run the smallest relevant repository check for shared code changes. For
example, changing ClientConfig or NFCLI launch behavior is broader than the
NFWeb-focused test suite.
Minimum Test Matrix¤
| Change | Required verification |
|---|---|
| Config field | Config validation test and NFWeb tests |
| Adapter or worker payload | Collector fixture with success, missing data, and worker error |
| Merge or identity rule | Forward/reverse and multi-link tests with exact IDs |
| History schema or retention | Round trip, scope filtering, cleanup, and restart behavior |
| Monitoring sample or retention | Existing-interface payload merge, missing data, exact cutoff, and no persistence |
| HTTP or WebSocket contract | Server test plus matching TypeScript type |
| React calculation or formatting | Vitest, type-check, and production build |
| WebGL graph lifecycle | Production build and browser validation |
| Packaged asset behavior | Build, serve through Tornado, and hard-refresh test |
| Documentation navigation | MkDocs build |
Focused Vitest coverage exists for graph calculations and inspector formatting. There is currently no automated WebGL browser suite, so manually validate graph lifecycle changes in a WebGL-capable browser.
Use Focused Change Playbooks¤
Add or Change a Topology Field¤
- Update the appropriate model in
topology/models.py. - Populate or merge it in
layers.pyorcollector.py. - Update
frontend/src/types.ts. - Update filtering, rendering, or inspector code only where the field is used.
- Add a collector or server assertion for the serialized value.
- Build the frontend and update user documentation when behavior changes.
Add a Topology Layer¤
- Add a strict configuration switch.
- Implement
TopologyLayerAdapterand return aLayerPatch. - Choose stable node and link identities before adding presentation code.
- Add it to
enabled_adapters()in dependency order. - Add its label and colour to the frontend.
- Test worker failures, partial payloads, deduplication, and merge behavior.
- Document its NORFAB source and operational meaning.
Do not make an independent scheduler or database for a layer. Those are application responsibilities.
Add a Browser Operation¤
- Confirm the operation is narrowly scoped to one application.
- Add a Tornado handler under
/api/v1/<application>/.... - Validate the operation's input and return a presentation-oriented response.
- Add the matching method and type in
api.tsandtypes.ts. - Add success, invalid input, conflict, and cross-origin tests as applicable.
Add or Change a Monitoring Metric¤
- Add and document the value in the appropriate Pydantic model in
norfab/core/monitoring.py. - Add the presentation field to
monitoring/models.pyandfrontend/src/types.ts. - Merge it in
MonitoringCollector.collect_once()without inventing fallbacks. - Add it directly to the existing ECharts options, details, or worker table.
- Extend collector, server, and contract-parity tests.
Do not introduce broker instrumentation, payload capture, a telemetry journal, or a database merely to add a dashboard metric. Such work requires a separate design decision because it changes the deliberately lightweight monitoring boundary.
Add Another NFWeb Application¤
- Create an application-owned Python package, models, configuration, and state.
- Add namespaced routes and contracts; do not expose raw worker results.
- Reuse the shared native client, network listener, optional database location, browser policy, and lifecycle.
- Give the application its own persistence only where required; in-memory state is sufficient for disposable monitoring history.
- Add a frontend view and navigation entry without forcing its domain into topology models.
- Register the module in
runtime.py; the shared server discovers its routes, health, startup, and shutdown behavior throughNFWebApplicationModule.
The Python host is application-neutral. The current frontend uses a small hash-based view selection between Monitoring and Topology. Keep it direct until additional applications require a more general registry or router.
Troubleshoot Efficiently¤
Collection Finishes but the Graph Is Empty¤
Check, in order:
- the active selected-device count;
- snapshot status and node/link counts;
- Collection events and layer errors;
- active search, health, and layer filters;
- raw worker payload shape against the adapter fixture;
- browser console errors.
A successful job does not guarantee its payload matches the adapter's expected shape.
Multiple Connection Records Appear¤
Physical records with the same normalized endpoints merge and union their
origins. Rendered links are then bundled per device pair and layer, while the
inspector retains parallel interface records. Inspect each record's layer, ID,
origin, device names, and interface names. Common valid cases are:
- separate data and management links;
- two real parallel interfaces.
For reverse LLDP advertisements, canonical IDs are identical when device aliases resolve uniquely to the selected scope and interfaces use a supported common short or long spelling. If they are not, compare hostname qualification, ambiguous aliases, and vendor-specific interface names. Do not deduplicate the stored data by device pair because real parallel interfaces must remain distinct.
The Graph Rearranges on Every Snapshot¤
Confirm that node IDs remain stable, coordinates are saved before replacing the
snapshot, and new graphData nodes receive saved coordinates. Do not call
zoomToFit() on every update.
The Browser Loads an Old UI¤
Run npm run build, inspect static/index.html for the new hashed asset name, and
hard-refresh the browser. Static hashed assets are immutable; the HTML shell is
served with no-cache.
The WebSocket Disconnects¤
Confirm the backend is running and inspect the browser network panel. The frontend reconnects with exponential backoff up to ten seconds.
Known Gaps¤
Keep these visible when planning work:
- View selection is intentionally a small hash-based switch rather than a routing dependency; revisit it only when navigation requirements become more complex.
- TypeScript contracts remain manually authored, although an automated field-level parity test now prevents silent drift from the authoritative Pydantic models.
- Graph calculations have focused Vitest coverage, but there is no automated WebGL/browser lifecycle suite.
- The lazy WebGL chunk remains inherently large; it is excluded from the initial application bundle and covered by an explicit Vite warning threshold.
- LLDP canonicalization covers case, FQDN variants, whitespace, and common interface abbreviations. Ambiguous device aliases and vendor-specific interface spellings are deliberately preserved rather than guessed.
Empty Overview and Admin navigation sections are reserved extension points, not incomplete application behavior.
Do not solve these gaps incidentally during an unrelated change. Make the smallest coherent change, add tests for the intended behavior, and update this list when a gap is deliberately resolved.
Definition of Done¤
Before handing NFWeb work to another developer or coding agent:
- requested behavior is implemented without weakening the boundaries above;
- Python and TypeScript contracts agree;
- focused Python tests pass;
- the frontend production build passes when frontend source changed;
- generated static assets are updated when frontend source changed;
- browser behavior is manually checked for graph lifecycle changes;
- user documentation and this guide reflect changed behavior;
- unrelated working-tree changes are preserved;
- the handoff states what changed, what was verified, and any remaining limitation.