A curses TUI for watching NFSv4 traffic on the server. Shows, per connected client, live: total bytes + delta since start, B/s rates, connection count, connection age, NFS version, mounted path, TCP RTT, sparkline trend, and how long ago a client was last seen. Includes filtering, sorting, pause, follow, /24 subnet aggregation, CSV logging, alerts, watchdog (disconnect logging), themes (8-color base + 256-color aliases), and persistent configuration.
NFS Traffic port:2049 14:32:07 interval:2s since:09:14:11 (05:17:56)
HOST CONNS CONNECTED SENT ΔSENT B/s SENT
app-01.xxx.tld 8 04:21:33 4.2 GiB 1.1 GiB 1.4 MiB/s
app-02.xxx.tld 8 04:18:02 3.8 GiB 892.3 MiB 2.1 MiB/s
batch-03.xxx.tld 4 03:55:11 821.4 MiB 120.5 MiB 340.2 KiB/s
...
clients:14 total conns:48 sent:18.4 GiB recv:6.7 GiB sort:sent↓ /:filter o:opts ...
- Prerequisites
- Installation
- Usage
- TUI layout
- Hotkeys (full reference)
- The o-popup (Options)
- Column reference
- Sorting
- Filtering and grouping
- Selection, follow, and ghost row
- Last-seen clients
- Bright sorted column
- Snapshot dumps
- CSV logging
- Alerts
- Watchdog (disconnect logging)
- Configurable TREND length
- Themes and colors
- Configuration files
- Architecture and data sources
- Permissions
- Compatibility
- Known limitations
- Version and author
- Linux kernel ≥ 5.3 (for the
ss -tiRTT stats; on older kernels the RTT column stays empty, everything else still works) - Python ≥ 3.8 with the
cursesmodule (standard on every distro Python) iproute2(thessbinary) onPATH- Read access to
/proc/fs/nfsd/clients/*/{info,states}for the NFSv version + mount path detection — i.e. run asrootor with the equivalent capabilities - A terminal with ≥ 8 colors (better:
$TERMcontains-256colorso the 256-color aliases and themes render correctly)
Not required: pip packages — everything is from the standard library.
nfsmon is a single Python script. Three steps:
# 1. Copy the script to the server (or via scp / Ansible)
sudo cp scripts/nfsmon.py /usr/local/bin/nfsmon
sudo chmod +x /usr/local/bin/nfsmon
# 2. Optional: system-wide defaults
sudo cp scripts/nfsmon.conf.example /etc/nfsmon.conf
# 3. Optional: per-user theme collection
mkdir -p ~/.config/nfsmon/colors
cp scripts/themes/*.conf ~/.config/nfsmon/colors/Without steps 2 + 3 the tool starts with built-in defaults — themes
can still be selected from the popup as soon as files exist in
~/.config/nfsmon/colors/.
sudo nfsmon # start the tool (curses TUI)
nfsmon --version # print version + author and exit
nfsmon -V # short form
nfsmon --help # argparse helpQuit anytime with q or Ctrl-C.
┌──────────────────────────────────────────────────────────────────┐
│ NFS Traffic port:2049 14:32:07 interval:2s since:09:14:11 ...│ ← Title row
│ HOST CONNS CONNECTED SENT ΔSENT B/s SENT ... │ ← Header (bold + underline)
│ app-01.xxx.tld ... │ ← Data rows (one per client)
│ ... │
│ stale-99.xxx.tld ... 10m 5s │ ← Ghost rows (last-seen)
│ │
│ clients:14 total conns:48 ... sort:sent↓ /:filter o:opts ...│ ← Footer (status + hint)
└──────────────────────────────────────────────────────────────────┘
- Title row — port, current time, tick interval, program start time
- Header row — active columns in their on-screen order; HOST flips to IP when the
Shift+itoggle is active - Data rows — one per live NFS client; each row color-coded by throughput tier (idle / low / medium / high / alert)
- Ghost rows — only when
Show last-seen clientsis on: clients that were once connected but aren't anymore — always below the live rows - Footer — totals on the left (clients/conns/sent/recv), status + hotkey hint on the right
Throughput color tiers (with the default theme):
- idle — no activity (dimmed)
- low — < 1 MB/s (green)
- medium — 1–10 MB/s (yellow, bold)
- high — ≥ 10 MB/s (red, bold)
- alert — over threshold for ≥ duration (red, bold, blink)
Also reachable in the tool via h.
| Key | Effect |
|---|---|
s |
Sort by SENT (bytes total) |
Shift+s |
Sort by ΔSENT (bytes since baseline) |
r |
Sort by RECV (total) |
Shift+r |
Sort by ΔRECV |
b |
Sort by B/s SENT (live rate) |
Shift+b |
Sort by B/s RECV |
c |
Sort by CONNS |
a |
Sort by CONNECTED (connection age) |
Shift+a |
Sort by ACTIVITY (idle first) |
i |
Sort by IP |
Shift+h |
Sort by HOST (hostname) |
n |
Sort by NFSv |
m |
Sort by MOUNT |
t |
Sort by RTT (latency) |
l |
Sort by SEEN (last seen) |
v |
Toggle sort direction (↑/↓) |
TREND (sparkline) intentionally has no hotkey — sparkline strings
aren't meaningfully comparable.
| Key | Effect |
|---|---|
/ |
Filter by host or IP substring |
↑ / ↓ |
Move selection cursor ±1 |
PgUp / PgDn |
Move selection ±5 rows |
Enter |
Detail popup for the selected row |
f |
Follow mode (pin selection to an IP) |
g |
Group by /24 subnet |
Shift+i |
HOST column: hostname ↔ IP toggle |
| Key | Effect |
|---|---|
o |
Open Options popup |
Shift+d |
Snapshot dump → /tmp/nfsmon_snapshot.txt |
space |
Pause / resume the display |
z |
Baseline reset (zero Δ + connection age) |
| Key | Effect |
|---|---|
h |
Toggle help popup |
q |
Quit |
Esc |
Close popup / cancel input |
Three tabs, layout 70×40 (or smaller, scaled to terminal size).
Navigation: ←/→ switches tabs (in the main area), ↑/↓ selects
rows, space toggles/activates, Tab jumps to the
[Save Settings] button at the bottom-right.
┌────── Options ─────────────────────────────────────────────────────┐
│ [defaults] [columns] [sort] │
│ │
│ Interval (s): [ 2] │
│ Port: [ 2049] │
│ CSV Log: [ ] /var/log/nfsmon.csv │
│ Alert MB/s: [ 10] │
│ Alert duration (s): [ 5] │
│ Theme: ‹ default › (space to cycle) │
│ Bright sorted column: [x] │
│ Show last-seen clients: [ ] │
│ Watchdog (log disconnects): [ ] /var/log/nfsmon-events.log │
│ TREND length: [10] (10-40) │
│ │
│ Tab:save ←→:tabs ↑↓:sel space:toggle Esc:close [Save Settings] │
└────────────────────────────────────────────────────────────────────┘
Per-program settings:
- Interval (s) — how often (in s) data is refreshed. Range 1–9999.
- Port — which TCP port to filter as the "NFS server port". Default 2049.
- CSV Log — toggle. When active, one row per client is appended each tick to
/var/log/nfsmon.csv(or the configured path). - Alert MB/s — throughput threshold above which the alert logic fires.
- Alert duration (s) — how long a client must stay above the threshold before being marked as alerting.
- Theme — active theme.
spacecycles through(default)+ every file in~/.config/nfsmon/colors/. The change is visible immediately. - Bright sorted column — toggle. The active sort column is rendered in its bright variant (A_BOLD, A_DIM stripped).
- Show last-seen clients — toggle. Previously-seen clients show up below the live ones as ghost rows.
- Watchdog (log disconnects) — toggle. Writes one CSV line per disconnect event to
/var/log/nfsmon-events.log(or the configured path). Path comes from[watchdog] pathin the config. - TREND length — numeric edit (10-40). Controls both the column width of the TREND column and the length of the per-IP sample buffer (
rate_history). Default 10, max 40 — values out of range are clamped to the default at startup.
One toggle row per column from COLUMNS. [x] = visible, [ ] = hidden. space flips it.
One radio row per column plus IP and ACTIVITY (idle first w/ v). space sets sort_key to the highlighted entry. The hotkeys (s/r/b/...) are direct shortcuts for exactly this.
Bottom-right of every tab. Tab toggles between the main area and the button:
Enteron the button → save + close popupspaceon the button → save + popup stays open
Always saves to ~/.config/nfsmon.conf. /etc/nfsmon.conf is never overwritten.
| Column | Default | Content |
|---|---|---|
HOST |
on | DNS name (reverse lookup), or IP when Shift+i is active |
NFSv |
off | Negotiated NFS version (4.2, 4.1, ...) from /proc/fs/nfsd/clients/*/info |
MOUNT |
off | Longest common path prefix of the client's mounted paths |
RTT |
off | TCP RTT (averaged over all active connections, in ms) from ss -ti |
CONNS |
on | Number of active TCP connections from this client |
CONNECTED |
on | How long the client has been continuously connected (HH:MM:SS or Xd HH:MM:SS) |
SEEN |
off | How long ago the client was last live (now, 15s, 5m 30s, 1h, 3d). For live clients: now. For ghost clients: time since the last tick they showed up in ss |
SENT |
on | Bytes the server has sent to the client (total, since the last tcp-stat reset) |
ΔSENT |
on | Difference vs. baseline (program start or z) |
B/s SENT |
off | Current sending rate (bytes per second) |
RECV |
on | Bytes received from the client (total) |
ΔRECV |
on | Difference vs. baseline |
B/s RECV |
off | Current receiving rate |
TREND |
off | Sparkline of the combined rate over the last N ticks. Width N is configurable via [defaults] → TREND length (range 10-40, default 10) |
sort_keyis the current sort key,sort_revthe direction.- Hotkeys set
sort_keyandsort_rev=True(descending). Shift+ais the special case:sort_key="activity",sort_rev=False— idle clients first.vtoggles only the direction (sort_rev), keeping the current key.- Footer shows the current sort as
sort:<key>↓orsort:<key>↑.
With Bright sorted column active, the column corresponding to the sort is also re-rendered per row with (attr & ~A_DIM) | A_BOLD effectively the bright variant of the role.
/opens an input-line mode in the footer. The substring is matched againsthostandip(lowercase).Escdiscards,Enterapplies.- While filtering, the cursor stays visible (
/<text>_) and the footer showsEnter:apply Esc:cancel. - An active filter shows up in the hint as
filter:<text>. Open the filter again with/; an empty string clears it. gtoggles group by /24. IPs are bucketed into10.0.10.0/24groups; numeric fields are summed,ageis the oldestfirst_seenin the group.RTTstays empty (aggregation isn't meaningful).
Filter and grouping apply to live rows. For ghost rows (last-seen): filter is applied, grouping is not — ghosts stay individually identifiable at the bottom.
↑/↓/PgUp/PgDnsetselected_ip. The selection is marked withA_REVERSE.- The selection auto-hides after 5 s of no input (except in follow mode).
Enteron the selection opens a detail popup with extra per-client info (NFSv, mount, etc., depending on whatfetch_detailreturns).ftoggles follow: the selection is pinned to the current IP (no auto-hide, no drop on disconnect). When the followed IP falls out of the visible set, its last snapshot row is rendered as a ghost row at the bottom (dimmed + reversed).
Note: this follow-ghost row is independent of the last-seen feature (see next section). The follow-ghost is one IP you're explicitly following. Last-seen ghosts are all IPs that have ever connected.
Toggle: o → [defaults] → Show last-seen clients → space.
When active, clients that previously appeared in ss but no longer do are shown as additional rows below the live clients — with all their last-known values (HOST, SENT, RECV, ΔSENT, ΔRECV) and B/s = 0, CONNS = 0, activity = 0. The SEEN column shows how long ago that
was (now, 15s, 5m 30s, 1h 25m, 3d 5h).
Two state variables hold this in memory:
last_seen: Dict[str, float]— IP → timestamp of latest appearanceseen_snapshot: Dict[str, Dict]— IP → last concrete data snapshot
Both are updated each tick for every live IP. The stale-IP cleanup in the tick (which drops first_seen, baseline, prev_totals for IPs no longer live) deliberately does not touch these two. Sorting: live rows are sorted by sort_key; ghost rows are also
sorted by sort_key — but the ghost block always stays below the live block, regardless of direction.
l is the direct hotkey for "Sort by SEEN".
Memory: last_seen/seen_snapshot grow unbounded (one entry per IP we've ever seen). That's intentional — once seen, an IP stays discoverable. In practice you get a few dozen to a few hundred IPs per day; even after weeks that's only a few MB. If you ever need a cap (e.g. only the last 24 h), add a cleanup loop every N ticks.
Toggle: o → [defaults] → Bright sorted column → space.
When active, the currently active sort column is re-rendered per data row with:
bright_attr = (attr & ~curses.A_DIM) | curses.A_BOLDEffect: A_DIM is stripped, A_BOLD is added — on most terminals the column appears in the bright variant of its role. Idle rows are lifted out of the dimmed tone into full intensity; color tiers move to their bright versions. Special case: when Shift+i is active (HOST column shows IP) and you sort by i/IP, the HOST column (which is currently displaying IP) is treated as the "active sort column".
Shift+d writes a plain-text dump of the currently visible view to /tmp/nfsmon_snapshot.txt. The content mirrors exactly what's on screen — same filter, same sort, same columns, same IP/HOST display. Use case: pinning the current state without taking a screenshot, e.g. to attach to tickets, emails, bug reports. The footer flashes a confirmation with the file path for 3 s.
Toggle: o → [defaults] → CSV Log → space. Default: off.
When on, the tool writes one row per client per tick to /var/log/nfsmon.csv (or the path set in the config). Format:
timestamp,ip,host,sent,recv,dsent,drecv,rate_sent,rate_recv,conns
2026-05-05T14:32:07,10.0.10.42,vp-app-01.xxx.tld,4523846542,1284567890,1182937462,403215100,1456823.45,512311.10,8- Header is only written when the file is freshly created or empty
f.flush()after every tick (no data loss on crash, but more disk writes)- OSError on write (full disk, permission denied) → toggle stays on, writes silently fail
Analysis e.g. with pandas:
import pandas as pd
df = pd.read_csv("/var/log/nfsmon.csv", parse_dates=["timestamp"])
df.groupby("ip")["rate_sent"].mean().sort_values(ascending=False).head(10)Trigger: per client, activity = rate_sent + rate_recv is measured
each tick. If it stays ≥ alert_mb * 1024 * 1024 (B/s) for at least
alert_dur seconds, the client is added to alert_active.
- The data row is rendered with the
alertrole (default red, bold, blink) - The footer shows
ALERT:<n>— how many clients are currently alerting - As soon as the rate drops below the threshold, the alert is reset
Thresholds are editable in the popup (Alert MB/s, Alert duration (s))
and persisted via the config ([alerts] section: threshold_mb,
duration_sec).
Toggle: o → [defaults] → Watchdog (log disconnects) → space. Default: off.
When on, every disconnect is written as a CSV line into an event
log file (default /var/log/nfsmon-events.log, path configurable via
[watchdog] path). Format:
timestamp,event,ip,host,last_sent,last_recv
2026-05-05T14:32:07,disconnect,10.0.10.42,vp-app-01.xxx.tld,4523846542,1284567890Detection: each tick, before the stale-IP cleanup, the tick block
computes set(first_seen.keys()) - active_ips. IPs that were live in
the previous tick and aren't anymore → disconnect. One row per IP;
host + last_sent + last_recv come from seen_snapshot (what the
previous tick stored).
Intentional limitations:
- Disconnects only, no reconnects. If you need the inverse (e.g.
"IP X was gone for Y seconds and is back"), it's available via
cross-reference between
last_seen(timestamp in seen_snapshot) and the current live list — reconnect detection would be a ~10-line addition. - No "flap filter". If a client bounces every tick, every disappearance writes a separate line. If needed: add a threshold/suppression layer.
Toggle semantics:
- Toggle-off closes the file handle.
- Toggle-on opens via
_open_watchdog()(header for new/empty files, append otherwise). At startup: if config sayswatchdog_enabled = true, the file is opened automatically — silent fail on OSError, the toggle stays OFF in that case.
Analysis e.g. with awk or pandas:
# Top 10 hosts by number of disconnects in the last 24 h
awk -F, -v cutoff="$(date -d '24 hours ago' -u +%Y-%m-%dT%H:%M:%S)" \
'$1 > cutoff {print $4}' /var/log/nfsmon-events.log \
| sort | uniq -c | sort -rn | headEdit field: o → [defaults] → TREND length → space → digits → Enter.
Range 10–40, default 10.
Sets both the column width of the TREND column and the buffer
length of the per-IP sample stack (rate_history). Both share the
same value (spark_len) so the column shows exactly as many samples
as it tracks.
Mechanics:
_build_active_cols(cols_visible, show_ip_in_host, spark_len)substitutes the TREND column width (single source of truth for the renderer +write_snapshot).sparkline(values, length=spark_len)renders that many bars.- Tick logic:
if len(h) > spark_len: del h[: len(h) - spark_len]. - When growing 10 → 40, it takes N-1 ticks for the new buffer to fill; until then you see leading whitespace — by design.
Persistence: [general] spark_len = N in ~/.config/nfsmon.conf.
Out-of-range values from the config are clamped to the default at
main() startup; no crash.
Caveat: on a small terminal with many columns enabled +
spark_len=40, horizontal space may be tight — columns at the right
edge get cropped (safe_addstr swallows it; no crash, but information
is lost).
A theme is a *.conf file with a [colors] section in
~/.config/nfsmon/colors/. Selectable in the tool via o →
[defaults] → Theme: → space cycling. The built-in default is
called (default) and lives in the code (DEFAULT_COLORS in
nfsmon.py).
| Role | Where it appears | Default |
|---|---|---|
title |
Title row + popup borders + popup titles | cyan,bold |
key_bar |
Tab bar in the o-popup, save button (focused), active tab | yellow,bold |
text |
Popup body text, inactive tabs (with dim) | green |
footer |
Bottom status row + help headers | cyan |
alert |
Row whose throughput exceeds the alert threshold | red,bold,blink |
idle |
Rows with no throughput, ghost row | white,dim |
activity_low |
< 1 MB/s | green |
activity_medium |
1–10 MB/s | yellow,bold |
activity_high |
≥ 10 MB/s | red,bold |
<role> = <fg_color>[,<attr>[,<attr>...]]
<fg_color> is one of:
- a name from the 8-color base (
black,red,green, ...) - a 256-color alias (
orange,pink,gold,mint,darkgray, ...) - a numeric code (e.g.
208,34)
<attr> values are comma-separated: bold, dim, blink, reverse,
underline, standout, normal.
Full list: see color.md next to this file.
In scripts/themes/ (drop-in copy):
default.conf— built-in defaults made explicit (8-color base)mono.conf— no colors, attributes only (for 8-color terminals or screen sharing)nord.conf— cool nordic blue/cyandracula.conf— purple/pink/cyan on darkgruvbox-dark.conf— warm yellow/orangetokyo-night.conf— deep blue with cyan/magentamonokai.conf— magenta/green/yellow classicmatrix.conf— all green, graded 22 → 46amber-mono.conf— amber CRT, graded 130 → 220cyberpunk.conf— neon magenta/cyan/pinksolarized-dark.conf— official Solarized codes
DEFAULT_COLORS(in code)[colors]from/etc/nfsmon.conf[colors]from~/.config/nfsmon.conf[colors]from~/.config/nfsmon/colors/<theme>.conf(whenthemeis set)
Themes override the user config. If you want custom colors
independent of any theme, put them in [colors] of nfsmon.conf and
leave theme = empty.
On terminals where curses.COLORS < 256, codes ≥ COLORS are
silently reverted per role to the role's built-in default. If the
default also doesn't fit, it falls back to -1 (terminal default fg).
No crash, no warning — just less color.
/etc/nfsmon.conf— system defaults~/.config/nfsmon.conf— user override (wins on duplicate keys)
save_config always writes to ~/.config/nfsmon.conf —
/etc/nfsmon.conf is never overwritten by the tool.
INI via Python's configparser. Full example file:
scripts/nfsmon.conf.example. The relevant sections:
[general]
interval = 2
port = 2049
group_subnet = false
show_ip = false
sort_key = sent
sort_rev = true
theme = nord
bright_sort_col = true
show_seen_clients = false
watchdog_enabled = false
spark_len = 10
[csv]
enabled = false
path = /var/log/nfsmon.csv
[columns]
host = true
nfsv = false
mount = false
rtt_avg = false
last_seen = false
conns = true
age = true
sent = true
dsent = true
rate_sent = false
recv = true
drecv = true
rate_recv = false
spark = false
[alerts]
threshold_mb = 10
duration_sec = 5
[watchdog]
path = /var/log/nfsmon-events.log
[colors]
title = cyan,bold
key_bar = yellow,bold
text = green
footer = cyan
alert = red,bold,blink
idle = white,dim
activity_low = green
activity_medium = yellow,bold
activity_high = red,bold# ~/.config/nfsmon/colors/<theme>.conf
[colors]
title = 33,bold
key_bar = 37,bold
# ... more rolesOnly the [colors] section is read. Other sections in a theme file
are ignored.
nfsmon is a single Python script with no external pip dependencies.
Per-tick data sources:
-
ss --no-header --tcp -tinp dport == :<port>- Per connection: source IP/port, bytes-sent, bytes-recv,
tcpi_rttfrom the-iflag (RTT in ms) - Aggregation per source IP: connections summed, bytes summed, RTT averaged
- Per connection: source IP/port, bytes-sent, bytes-recv,
-
/proc/fs/nfsd/clients/<id>/info(kernel ≥ 5.3)- Per client:
address: "<ip>:<port>",minor version: <n> - Yields the NFS version →
c["nfsv"]
- Per client:
-
/proc/fs/nfsd/clients/<id>/states- Per client: all mounted paths (as
"…"quoted strings) nfsmonextracts the longest common directory prefix →c["mount"]
- Per client: all mounted paths (as
-
socket.gethostbyaddr(ip)(in a daemon thread, cached)- Reverse DNS for
c["host"]. On failure: shows the IP string.
- Reverse DNS for
Tick order:
- Parse ss output →
raw - Set
first_seen,baselinefor new IPs - Stale cleanup: drop IPs no longer active from
first_seen,baseline,prev_totals,rate_history,alert_since,alert_active(but not fromlast_seen/seen_snapshot) - Compute per-IP rates (delta vs. previous tick / dt)
- Advance the per-IP sparkline history
- Read NFSd info from
/proc/fs/nfsd(NFSv + mount) - Merge everything into the
connslist last_seen[ip] = now+seen_snapshot[ip] = ...for every live IP- Write a CSV row per client (when enabled)
- Alert detection (threshold/duration)
UI loop (every 100 ms):
- Drain + process keys
- Compute the visible list (filter/group/sort + ghost append)
draw()to redraw
nfsmon needs:
- Root or equivalent for
/proc/fs/nfsd/clients/. Without that access,NFSvandMOUNTstay empty — the rest still works. /var/log/nfsmon.csvwritable (when CSV logging is on). The default path is root-only writable; for non-root, configure a path under$HOME./var/log/nfsmon-events.logwritable (when watchdog is on). Default path is root-only too; redirect via[watchdog] pathto e.g.~/nfsmon-events.log./tmp/nfsmon_snapshot.txtwritable (for theShift+ddump). The standard/tmpis fine.~/.config/nfsmon/writable (for persistent config).
| Component | Requirement | Behavior on violation |
|---|---|---|
| Linux kernel | ≥ 5.3 for RTT | Older kernels: RTT column empty, otherwise OK |
| Linux kernel | ≥ 5.3 for /proc/fs/nfsd/clients/ |
Older: NFSv/MOUNT empty |
| Python | ≥ 3.8 | Older: f-strings + type hints don't compile |
ss binary |
iproute2 present | Missing: tool starts but shows 0 connections |
| Terminal colors | ≥ 8 | Fewer: themes fall back to -1 (default fg) |
| Terminal colors | < 256 | Codes ≥ COLORS revert per role to the built-in default |
nconnect (client) |
optional | More CONNS per client when active (≥ 5.3 client kernel) |
- NFSv3 clients are not shown.
nfsmonfilters by default ontcp dport == 2049(the standard NFSv4 port); v3 runs over rpcbind/portmap with dynamic ports and isn't picked up. Workaround: setportin the popup to the v3 port. last_seen/seen_snapshotgrow unbounded. One entry per IP ever seen. Practically harmless (KB range), but on servers with thousands of distinct IPs per day this might matter. A cleanup pass would have to be added.- Group-by /24 + last-seen clients: ghost rows are deliberately not grouped. That's intentional (identifiability), but it means with /24 aggregation the live block is denser while the full ghost block hangs unaggregated below.
- Pause does not freeze data collection.
spaceonly freezes the display (frozen_connsas a snapshot). Collection keeps ticking in the background (otherwise resume would cause a rate spike). Consequence: on resume,last_seen/ ghosts are more recent than the last-shown view — which is consistent with the "display freezes, data doesn't" model. - CSV logging doesn't buffer. One
f.flush()per tick — robust against crashes, but with many clients (hundreds) and a low interval (1 s) you'll notice the disk load. - The reverse-DNS cache is process-local. A restart wipes the cache. With slow DNS you'll briefly see IPs instead of hostnames until the daemon thread catches up.
- The
Shift+itoggle affects only the HOST column, not the detail popup. The detail popup always shows the IP plus the hostname (if known) — independent of the toggle.