diff --git a/README.md b/README.md index 80096f2..84a430f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MeshCore Hub -Python 3.11+ platform for managing and orchestrating MeshCore mesh networks. +Python 3.13+ platform for managing and orchestrating MeshCore mesh networks.  diff --git a/src/meshcore_hub/api/routes/advertisements.py b/src/meshcore_hub/api/routes/advertisements.py index 7ed44a7..0ba0731 100644 --- a/src/meshcore_hub/api/routes/advertisements.py +++ b/src/meshcore_hub/api/routes/advertisements.py @@ -96,6 +96,9 @@ async def list_advertisements( received_by: Optional[str] = Query( None, description="Filter by receiver node public key" ), + member_id: Optional[str] = Query( + None, description="Filter by member_id tag value of source node" + ), since: Optional[datetime] = Query(None, description="Start timestamp"), until: Optional[datetime] = Query(None, description="End timestamp"), limit: int = Query(50, ge=1, le=100, description="Page size"), @@ -143,6 +146,16 @@ async def list_advertisements( if received_by: query = query.where(ReceiverNode.public_key == received_by) + if member_id: + # Filter advertisements from nodes that have a member_id tag with the specified value + query = query.where( + SourceNode.id.in_( + select(NodeTag.node_id).where( + NodeTag.key == "member_id", NodeTag.value == member_id + ) + ) + ) + if since: query = query.where(Advertisement.received_at >= since) diff --git a/src/meshcore_hub/api/routes/nodes.py b/src/meshcore_hub/api/routes/nodes.py index a8ca922..2e895b4 100644 --- a/src/meshcore_hub/api/routes/nodes.py +++ b/src/meshcore_hub/api/routes/nodes.py @@ -22,6 +22,7 @@ async def list_nodes( None, description="Search in name tag, node name, or public key" ), adv_type: Optional[str] = Query(None, description="Filter by advertisement type"), + member_id: Optional[str] = Query(None, description="Filter by member_id tag value"), limit: int = Query(50, ge=1, le=500, description="Page size"), offset: int = Query(0, ge=0, description="Page offset"), ) -> NodeList: @@ -48,6 +49,16 @@ async def list_nodes( if adv_type: query = query.where(Node.adv_type == adv_type) + if member_id: + # Filter nodes that have a member_id tag with the specified value + query = query.where( + Node.id.in_( + select(NodeTag.node_id).where( + NodeTag.key == "member_id", NodeTag.value == member_id + ) + ) + ) + # Get total count count_query = select(func.count()).select_from(query.subquery()) total = session.execute(count_query).scalar() or 0 diff --git a/src/meshcore_hub/web/routes/advertisements.py b/src/meshcore_hub/web/routes/advertisements.py index 2369210..a6c4000 100644 --- a/src/meshcore_hub/web/routes/advertisements.py +++ b/src/meshcore_hub/web/routes/advertisements.py @@ -15,6 +15,8 @@ router = APIRouter() async def advertisements_list( request: Request, search: str | None = Query(None, description="Search term"), + member_id: str | None = Query(None, description="Filter by member"), + public_key: str | None = Query(None, description="Filter by node public key"), page: int = Query(1, ge=1, description="Page number"), limit: int = Query(50, ge=1, le=100, description="Items per page"), ) -> HTMLResponse: @@ -30,12 +32,41 @@ async def advertisements_list( params: dict[str, int | str] = {"limit": limit, "offset": offset} if search: params["search"] = search + if member_id: + params["member_id"] = member_id + if public_key: + params["public_key"] = public_key # Fetch advertisements from API advertisements = [] total = 0 + members = [] + nodes = [] try: + # Fetch members for dropdown + members_response = await request.app.state.http_client.get( + "/api/v1/members", params={"limit": 100} + ) + if members_response.status_code == 200: + members = members_response.json().get("items", []) + + # Fetch nodes for dropdown + nodes_response = await request.app.state.http_client.get( + "/api/v1/nodes", params={"limit": 500} + ) + if nodes_response.status_code == 200: + nodes = nodes_response.json().get("items", []) + + # Sort nodes alphabetically by display name + def get_node_display_name(node: dict) -> str: + for tag in node.get("tags") or []: + if tag.get("key") == "name": + return str(tag.get("value", "")).lower() + return str(node.get("name") or node.get("public_key", "")).lower() + + nodes.sort(key=get_node_display_name) + response = await request.app.state.http_client.get( "/api/v1/advertisements", params=params ) @@ -58,6 +89,10 @@ async def advertisements_list( "limit": limit, "total_pages": total_pages, "search": search or "", + "member_id": member_id or "", + "public_key": public_key or "", + "members": members, + "nodes": nodes, } ) diff --git a/src/meshcore_hub/web/routes/nodes.py b/src/meshcore_hub/web/routes/nodes.py index 5810733..3a7f907 100644 --- a/src/meshcore_hub/web/routes/nodes.py +++ b/src/meshcore_hub/web/routes/nodes.py @@ -16,6 +16,7 @@ async def nodes_list( request: Request, search: str | None = Query(None, description="Search term"), adv_type: str | None = Query(None, description="Filter by node type"), + member_id: str | None = Query(None, description="Filter by member"), page: int = Query(1, ge=1, description="Page number"), limit: int = Query(20, ge=1, le=100, description="Items per page"), ) -> HTMLResponse: @@ -33,12 +34,22 @@ async def nodes_list( params["search"] = search if adv_type: params["adv_type"] = adv_type + if member_id: + params["member_id"] = member_id # Fetch nodes from API nodes = [] total = 0 + members = [] try: + # Fetch members for dropdown + members_response = await request.app.state.http_client.get( + "/api/v1/members", params={"limit": 100} + ) + if members_response.status_code == 200: + members = members_response.json().get("items", []) + response = await request.app.state.http_client.get( "/api/v1/nodes", params=params ) @@ -62,6 +73,8 @@ async def nodes_list( "total_pages": total_pages, "search": search or "", "adv_type": adv_type or "", + "member_id": member_id or "", + "members": members, } ) diff --git a/src/meshcore_hub/web/templates/_macros.html b/src/meshcore_hub/web/templates/_macros.html new file mode 100644 index 0000000..a994ef9 --- /dev/null +++ b/src/meshcore_hub/web/templates/_macros.html @@ -0,0 +1,47 @@ +{# Reusable macros for templates #} + +{# + Pagination macro + + Parameters: + - page: Current page number + - total_pages: Total number of pages + - params: Dict of query parameters to preserve (e.g., {"search": "foo", "limit": 50}) +#} +{% macro pagination(page, total_pages, params={}) %} +{% if total_pages > 1 %} +{% set query_parts = [] %} +{% for key, value in params.items() %} +{% if value is not none and value != '' %} +{% set _ = query_parts.append(key ~ '=' ~ value) %} +{% endif %} +{% endfor %} +{% set base_query = query_parts|join('&') %} +{% set query_prefix = '&' if base_query else '' %} +