From f2360f85fcf5c4ea795b05adb86c8462aee42ee3 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 13 Jul 2026 10:25:47 +0800 Subject: [PATCH] feat(marker): add project-level marker aggregation endpoint Add a read-only `markers` property on Project that flattens every link's markers into a single dict keyed by "{link_id}/{name}", each entry carrying the parent link_id and capture-side node_id. Expose it via GET /projects/{pid}/markers (Project.Audit) so the frontend can fetch all markers in one round-trip instead of enumerating links first. Also surface a marker count in project.stats(). --- gns3server/api/routes/controller/projects.py | 15 ++++++++++++ gns3server/controller/project.py | 25 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index 333053e3b..39c225594 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -203,6 +203,21 @@ def get_project_stats(project: Project = Depends(dep_project)) -> dict: return project.stats() +@router.get("/{project_id}/markers", dependencies=[Depends(has_privilege("Project.Audit"))]) +def get_project_markers(project: Project = Depends(dep_project)) -> dict: + """ + Return all traffic-insight markers across every link in the project. + + Each entry is keyed ``"{link_id}/{marker_name}"`` and carries the + marker's BPF, tag, color, enabled flag, plus its parent ``link_id`` + and capture-side ``node_id`` for frontend filtering / grouping. + + Required privilege: Project.Audit + """ + + return project.markers + + @router.post( "/{project_id}/close", status_code=status.HTTP_204_NO_CONTENT, diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index c0c0a262a..1c446082a 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -898,6 +898,30 @@ class Project: return self._get_closed_data("links", "link_id") return self._links + @property + def markers(self): + """ + Project-level read-only aggregation of all markers across every link. + + Each entry is keyed ``"{link_id}/{marker_name}"`` so the flat dict is + globally unique within the project. The value is a clone of the link's + per-marker dict plus ``link_id`` and ``node_id`` (the capture-side node) + for convenience — the frontend can filter/group by link or node without + extra round-trips. + + :returns: dict[str, dict] — keyed by "{link_id}/{marker_name}" + """ + result = {} + for link_id, link in self._links.items(): + for name, info in link.markers.items(): + key = f"{link_id}/{name}" + result[key] = { + **info, + "link_id": link_id, + "node_id": info.get("capture_node_id"), + } + return result + @property def snapshots(self): """ @@ -1710,6 +1734,7 @@ class Project: "links": len(self._links), "drawings": len(self._drawings), "snapshots": len(self._snapshots), + "markers": sum(len(link.markers) for link in self._links.values()), } def asdict(self):