feat(marker): add optional color field to marker data model

Add a user-chosen hex color (e.g. '#ff5722') to each marker, stored in
Link._markers alongside bpf/tag/enabled. The color:

- Is serialized with the link (asdict) and persisted in the topology, so
  it survives project reload and is consistent across devices.
- Never reaches uBridge (compute ignores it) — it is purely a Web UI
  concern.
- Can be updated independently of the BPF (color-only changes skip the
  ubridge round-trip).

MarkerCreate schema gains color: Optional[str]; start_marker and
update_marker carry it through; REST create/update endpoints pass it.

The marker.match WebSocket event stays lean (no color) — the Web UI maps
filter -> color from the link state it already holds.
This commit is contained in:
YueGuobin 2026-07-12 21:47:46 +08:00
parent b8f49696cc
commit 3f15ac38f6
No known key found for this signature in database
3 changed files with 27 additions and 7 deletions

View File

@ -454,12 +454,14 @@ async def create_marker(
Required privilege: Link.Modify
"""
name = marker_data.name or f"marker-{link.id[:8]}"
await link.start_marker(
name=marker_data.name or f"marker-{link.id[:8]}",
name=name,
bpf=marker_data.bpf,
tag=marker_data.tag,
color=marker_data.color,
)
return link.markers.get(marker_data.name, {})
return link.markers.get(name, {})
@router.delete(
@ -499,6 +501,7 @@ async def update_marker(
name=marker_name,
bpf=marker_data.bpf if marker_data.bpf else None,
tag=marker_data.tag,
color=marker_data.color,
)
return link.markers.get(marker_name, {})

View File

@ -335,13 +335,15 @@ class UDPLink(Link):
}
self._marker_capture_nodes[name] = capture_side
async def start_marker(self, name, bpf, tag=None):
async def start_marker(self, name, bpf, tag=None, color=None):
"""
Attach a traffic-insight marker to this link.
:param name: stable filter name echoed in MARK signals + pcap identity
:param bpf: libpcap BPF expression
:param tag: optional correlation id
:param color: optional hex color for the Web UI (e.g. '#ff5722'); stored
with the link and persisted in the topology, never sent to uBridge
"""
if name in self._markers:
@ -361,7 +363,7 @@ class UDPLink(Link):
data=data,
)
self._store_capture_node_for_marker(name, marker_side)
self._markers[name].update({"bpf": bpf, "tag": tag, "enabled": True})
self._markers[name].update({"bpf": bpf, "tag": tag, "enabled": True, "color": color})
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
@ -389,7 +391,7 @@ class UDPLink(Link):
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
async def update_marker(self, name, bpf=None, tag=None, enabled=None):
async def update_marker(self, name, bpf=None, tag=None, enabled=None, color=None):
"""
Update an existing marker. A BPF change requires delete+re-add so the
ubridge side flushes the pcap and the new filter takes effect.
@ -398,6 +400,7 @@ class UDPLink(Link):
:param bpf: new BPF expression (None = keep existing)
:param tag: new tag id (None = keep existing)
:param enabled: toggle (None = keep existing)
:param color: new hex color (None = keep existing)
"""
marker_info = self._markers.get(name)
@ -407,11 +410,15 @@ class UDPLink(Link):
new_bpf = bpf if bpf is not None else marker_info["bpf"]
new_tag = tag if tag is not None else marker_info.get("tag")
new_enabled = enabled if enabled is not None else marker_info.get("enabled", True)
new_color = color if color is not None else marker_info.get("color")
if not new_enabled and marker_info.get("enabled", True):
# Toggle off: remove from ubridge but keep state.
await self.stop_marker(name)
self._markers[name] = {**marker_info, "bpf": new_bpf, "tag": new_tag, "enabled": False}
self._markers[name] = {
**marker_info, "bpf": new_bpf, "tag": new_tag,
"enabled": False, "color": new_color,
}
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
return
@ -441,7 +448,13 @@ class UDPLink(Link):
),
data=data,
)
self._markers[name] = {**marker_info, "bpf": new_bpf, "tag": new_tag, "enabled": True}
self._markers[name] = {
**marker_info, "bpf": new_bpf, "tag": new_tag,
"enabled": True, "color": new_color,
}
elif new_color != marker_info.get("color"):
# Color-only change: no ubridge round-trip, just update state.
self._markers[name] = {**marker_info, "color": new_color}
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()

View File

@ -153,6 +153,10 @@ class MarkerCreate(BaseModel):
bpf: str
tag: Optional[int] = None
link_id: Optional[str] = None
color: Optional[str] = Field(
None,
description="User-chosen hex color for this marker in the Web UI, e.g. '#ff5722'"
)
class MarkerDelete(BaseModel):