feat(marker): add highlight_duration render hint

Per-marker UI hint (milliseconds, ge=1) for how long the Web UI keeps a
marker highlighted after a match. Mirrors color: stored on the link,
persisted in the topology, inherited via project-level definitions, and
never sent to uBridge. Omitted = null = frontend uses its own default.

The default is intentionally NOT set in the schema: MarkerCreate also
backs PUT updates, so a schema default would make every partial update
silently reset the value. None means "not provided" (keep existing on
update / frontend decides on create).
This commit is contained in:
YueGuobin 2026-07-13 23:32:08 +08:00
parent 179f072c33
commit 98bcd5eddd
No known key found for this signature in database
6 changed files with 40 additions and 8 deletions

View File

@ -464,6 +464,7 @@ async def create_marker(
bpf=marker_data.bpf, bpf=marker_data.bpf,
tag=marker_data.tag, tag=marker_data.tag,
color=marker_data.color, color=marker_data.color,
highlight_duration=marker_data.highlight_duration,
) )
return link.markers.get(name, {}) return link.markers.get(name, {})
@ -507,6 +508,7 @@ async def update_marker(
tag=marker_data.tag, tag=marker_data.tag,
color=marker_data.color, color=marker_data.color,
enabled=marker_data.enabled, enabled=marker_data.enabled,
highlight_duration=marker_data.highlight_duration,
) )
return link.markers.get(marker_name, {}) return link.markers.get(marker_name, {})

View File

@ -266,6 +266,7 @@ async def create_marker_definition(
bpf=def_data.bpf, bpf=def_data.bpf,
tag=def_data.tag, tag=def_data.tag,
color=def_data.color, color=def_data.color,
highlight_duration=def_data.highlight_duration,
) )
return project.marker_definitions.get(name, {}) return project.marker_definitions.get(name, {})
@ -290,6 +291,7 @@ async def update_marker_definition(
bpf=def_data.bpf if def_data.bpf else None, bpf=def_data.bpf if def_data.bpf else None,
tag=def_data.tag, tag=def_data.tag,
color=def_data.color, color=def_data.color,
highlight_duration=def_data.highlight_duration,
) )
return project.marker_definitions.get(def_name, {}) return project.marker_definitions.get(def_name, {})

View File

@ -122,6 +122,7 @@ class Link:
bpf=marker_def["bpf"], bpf=marker_def["bpf"],
tag=marker_def.get("tag"), tag=marker_def.get("tag"),
color=marker_def.get("color"), color=marker_def.get("color"),
highlight_duration=marker_def.get("highlight_duration"),
inherited_from=def_name, inherited_from=def_name,
) )

View File

@ -212,7 +212,7 @@ class Project:
self._allocated_node_names = set() self._allocated_node_names = set()
self._nodes = {} self._nodes = {}
self._links = {} self._links = {}
self._marker_definitions = {} # name → {bpf, tag, color} self._marker_definitions = {} # name → {bpf, tag, color, highlight_duration}
self._drawings = {} self._drawings = {}
self._snapshots = {} self._snapshots = {}
self._computes = [] self._computes = []
@ -926,11 +926,11 @@ class Project:
@property @property
def marker_definitions(self): def marker_definitions(self):
""" """
:returns: dict of project-level marker definitions (name → {bpf, tag, color}) :returns: dict of project-level marker definitions (name → {bpf, tag, color, highlight_duration})
""" """
return self._marker_definitions return self._marker_definitions
async def create_marker_definition(self, name, bpf, tag=None, color=None): async def create_marker_definition(self, name, bpf, tag=None, color=None, highlight_duration=None):
""" """
Create a project-level marker definition and fan out to every existing Create a project-level marker definition and fan out to every existing
link that has a capable node. Links without a capable node are silently link that has a capable node. Links without a capable node are silently
@ -942,12 +942,12 @@ class Project:
f"Marker definition '{name}' already exists in this project" f"Marker definition '{name}' already exists in this project"
) )
self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "color": color} self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "color": color, "highlight_duration": highlight_duration}
await self._apply_def_to_all_links(name) await self._apply_def_to_all_links(name)
self.dump() self.dump()
self.emit_notification("project.updated", self.asdict()) self.emit_notification("project.updated", self.asdict())
async def update_marker_definition(self, name, bpf=None, tag=None, color=None): async def update_marker_definition(self, name, bpf=None, tag=None, color=None, highlight_duration=None):
""" """
Update a marker definition and sync every inherited copy on every link. Update a marker definition and sync every inherited copy on every link.
""" """
@ -964,13 +964,16 @@ class Project:
d["tag"] = tag d["tag"] = tag
if color is not None: if color is not None:
d["color"] = color d["color"] = color
if highlight_duration is not None:
d["highlight_duration"] = highlight_duration
# Sync: update every inherited copy across all links. # Sync: update every inherited copy across all links.
for link in list(self._links.values()): for link in list(self._links.values()):
marker_name = f"global-{name}" marker_name = f"global-{name}"
if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name: if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name:
await link.update_marker( await link.update_marker(
marker_name, bpf=d["bpf"], tag=d.get("tag"), color=d.get("color"), inherited=True marker_name, bpf=d["bpf"], tag=d.get("tag"), color=d.get("color"),
highlight_duration=d.get("highlight_duration"), inherited=True
) )
self.dump() self.dump()
self.emit_notification("project.updated", self.asdict()) self.emit_notification("project.updated", self.asdict())

View File

@ -322,7 +322,7 @@ class UDPLink(Link):
# explicitly deletes a marker via the REST API, and a marker is torn # explicitly deletes a marker via the REST API, and a marker is torn
# down automatically only when its link is deleted. # down automatically only when its link is deleted.
async def start_marker(self, name, bpf, tag=None, color=None, inherited_from=None): async def start_marker(self, name, bpf, tag=None, color=None, highlight_duration=None, inherited_from=None):
""" """
Attach a traffic-insight marker to this link. Attach a traffic-insight marker to this link.
@ -337,6 +337,8 @@ class UDPLink(Link):
:param tag: optional correlation id :param tag: optional correlation id
:param color: optional hex color for the Web UI (e.g. '#ff5722'); stored :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 with the link and persisted in the topology, never sent to uBridge
:param highlight_duration: optional UI-only hint (milliseconds) for how
long a match keeps the marker highlighted; stored, never sent to uBridge
:param inherited_from: def name when this marker is a project-level :param inherited_from: def name when this marker is a project-level
inheritance copy; set automatically, never exposed to REST callers inheritance copy; set automatically, never exposed to REST callers
""" """
@ -354,6 +356,7 @@ class UDPLink(Link):
"tag": tag, "tag": tag,
"enabled": True, "enabled": True,
"color": color, "color": color,
"highlight_duration": highlight_duration,
"capture_node_id": marker_side["node"].id, "capture_node_id": marker_side["node"].id,
} }
if inherited_from: if inherited_from:
@ -391,7 +394,7 @@ class UDPLink(Link):
self._project.emit_notification("link.updated", self.asdict()) self._project.emit_notification("link.updated", self.asdict())
self._project.dump() self._project.dump()
async def update_marker(self, name, bpf=None, tag=None, enabled=None, color=None, inherited=False): async def update_marker(self, name, bpf=None, tag=None, enabled=None, color=None, highlight_duration=None, inherited=False):
""" """
Update an existing marker's BPF/tag/enabled/color. Any change pushes via Update an existing marker's BPF/tag/enabled/color. Any change pushes via
``self.update()``; uBridge picks up the new params on the next NIO ``self.update()``; uBridge picks up the new params on the next NIO
@ -402,6 +405,7 @@ class UDPLink(Link):
:param tag: new tag id (None = keep existing) :param tag: new tag id (None = keep existing)
:param enabled: toggle (None = keep existing) :param enabled: toggle (None = keep existing)
:param color: new hex color (None = keep existing) :param color: new hex color (None = keep existing)
:param highlight_duration: new UI highlight duration in ms (None = keep existing)
:param inherited: set by project-level sync to bypass the inheritance :param inherited: set by project-level sync to bypass the inheritance
guard (the project layer is the legitimate editor) guard (the project layer is the legitimate editor)
""" """
@ -428,6 +432,8 @@ class UDPLink(Link):
marker_info["enabled"] = enabled marker_info["enabled"] = enabled
if color is not None: if color is not None:
marker_info["color"] = color marker_info["color"] = color
if highlight_duration is not None:
marker_info["highlight_duration"] = highlight_duration
if self._created: if self._created:
await self.update() await self.update()

View File

@ -162,6 +162,15 @@ class MarkerCreate(BaseModel):
None, None,
description="User-chosen hex color for this marker in the Web UI, e.g. '#ff5722'", description="User-chosen hex color for this marker in the Web UI, e.g. '#ff5722'",
) )
highlight_duration: Optional[int] = Field(
None,
ge=1,
description=(
"How long (milliseconds) the Web UI keeps this marker highlighted "
"after a match. Omitted = use the UI default. Pure render hint — "
"stored on the link, never sent to uBridge."
),
)
enabled: Optional[bool] = Field( enabled: Optional[bool] = Field(
None, None,
description="Whether the marker is active. Defaults to true on creation.", description="Whether the marker is active. Defaults to true on creation.",
@ -196,6 +205,15 @@ class MarkerDefinitionCreate(BaseModel):
None, None,
description="User-chosen hex color for the marker in the Web UI, e.g. '#ff5722'", description="User-chosen hex color for the marker in the Web UI, e.g. '#ff5722'",
) )
highlight_duration: Optional[int] = Field(
None,
ge=1,
description=(
"How long (milliseconds) the Web UI keeps this marker highlighted "
"after a match. Omitted = use the UI default. Pure render hint — "
"stored with the definition, never sent to uBridge."
),
)
@field_validator("name") @field_validator("name")
@classmethod @classmethod