From f716e34bf5b62b3b31d8b3df6016fe37c1191e75 Mon Sep 17 00:00:00 2001 From: Louis King Date: Fri, 15 May 2026 19:18:35 +0100 Subject: [PATCH] fix: add public_key filter to advertisements API endpoint The node detail page sends a public_key query param when fetching recent advertisements, but the API silently ignored it (FastAPI ignores unknown query params). This returned the 10 most recent ads network-wide instead of for the specific node, making all dates appear as today. --- .../plan.md | 82 +++++++++++++++++++ .../tasks.md | 13 +++ src/meshcore_hub/api/routes/advertisements.py | 6 ++ tests/test_api/test_advertisements.py | 46 +++++++++++ 4 files changed, 147 insertions(+) create mode 100644 docs/plans/20260515-1900-recent-adverts-timestamps/plan.md create mode 100644 docs/plans/20260515-1900-recent-adverts-timestamps/tasks.md diff --git a/docs/plans/20260515-1900-recent-adverts-timestamps/plan.md b/docs/plans/20260515-1900-recent-adverts-timestamps/plan.md new file mode 100644 index 0000000..0a631e3 --- /dev/null +++ b/docs/plans/20260515-1900-recent-adverts-timestamps/plan.md @@ -0,0 +1,82 @@ +# Plan: Fix Recent Advertisements Date Always Showing Today + +**Date:** 2025-05-15 +**Status:** Draft +**Scope:** API only (advertisements endpoint) + +## Problem + +The "Recent Advertisements" section on the Node detail page always shows today's date, while the time portion is accurate. The root cause is a missing `public_key` query parameter filter on the API endpoint. + +## Root Cause Analysis + +### Missing `public_key` filter in advertisements API + +**Location:** `src/meshcore_hub/api/routes/advertisements.py:44` + +The node detail page fetches advertisements with: + +```js +// node-detail.js:86 +apiGet('/api/v1/advertisements', { public_key: publicKey, limit: 10 }) +``` + +But the `list_advertisements` endpoint does **not accept a `public_key` query parameter**. FastAPI silently ignores unknown query params, so the `public_key` filter is dropped. The API returns the 10 most recent advertisements across **all nodes** (sorted by `received_at DESC`), not filtered to the specific node. + +Since results are the most recent network-wide ads, they cluster around the current time — making it appear that all dates show today. + +### Sibling endpoints checked (no issues found) + +- **Telemetry**: Already has `node_public_key` filter (`telemetry.py:23`) — the frontend passes `node_public_key` which the API correctly applies +- **Messages**: Not called from the node detail page — no impact +- **Nodes**: Uses path parameter (`/api/v1/nodes/{public_key}`) — no issue + +## Plan of Changes + +### Step 1: Add `public_key` filter to advertisements API route + +**File:** `src/meshcore_hub/api/routes/advertisements.py` + +- Add a `public_key: Optional[str] = Query(None, ...)` parameter to `list_advertisements` +- Add a filter clause: `if public_key: query = query.where(Advertisement.public_key == public_key)` +- This filters by the **source** node's public key (the node that sent the advertisement) + +### Step 2: Update API tests + +**File:** `tests/test_api/test_advertisements.py` + +- Add a test for the `public_key` query parameter filtering +- Verify ads returned match the requested public key +- Verify ads with different public keys are excluded + +### Step 3: Verify frontend formatting + +No frontend changes needed. The `public_key` query param is already sent by `node-detail.js:86`. Once the API honors the filter, the node detail page will show only that node's most recent advertisements (which should span multiple dates, not just today). + +## Files Changed + +| File | Change | +|------|--------| +| `src/meshcore_hub/api/routes/advertisements.py` | Add `public_key` query parameter filter | +| `tests/test_api/test_advertisements.py` | Test `public_key` filter | + +## Testing + +```bash +source .venv/bin/activate +pytest tests/test_api/test_advertisements.py -v +pre-commit run --all-files +``` + +## Verification + +After deploying the fix: +1. Navigate to a node detail page with historical advertisement data +2. Verify the "Recent Advertisements" table shows ads spanning multiple dates (not all today) +3. Verify the ad count is limited to 10 and only belongs to the viewed node + +## Risk Assessment + +- **Low risk:** Adding an optional query parameter is backward-compatible +- **No migration needed:** No schema changes to database tables +- **No frontend changes needed:** The frontend already sends the parameter diff --git a/docs/plans/20260515-1900-recent-adverts-timestamps/tasks.md b/docs/plans/20260515-1900-recent-adverts-timestamps/tasks.md new file mode 100644 index 0000000..645cfe7 --- /dev/null +++ b/docs/plans/20260515-1900-recent-adverts-timestamps/tasks.md @@ -0,0 +1,13 @@ +# Tasks: Fix Recent Advertisements Date Always Showing Today + +## Implementation + +- [ ] Add `public_key: Optional[str] = Query(None)` parameter to `list_advertisements` in `src/meshcore_hub/api/routes/advertisements.py` +- [ ] Add filter clause `if public_key: query = query.where(Advertisement.public_key == public_key)` +- [ ] Add test for `public_key` query parameter filtering in `tests/test_api/test_advertisements.py` +- [ ] Verify test covers: ads match requested key, ads with other keys excluded + +## Validation + +- [ ] `pytest tests/test_api/test_advertisements.py -v` passes +- [ ] `pre-commit run --all-files` passes diff --git a/src/meshcore_hub/api/routes/advertisements.py b/src/meshcore_hub/api/routes/advertisements.py index c68526b..f4ff043 100644 --- a/src/meshcore_hub/api/routes/advertisements.py +++ b/src/meshcore_hub/api/routes/advertisements.py @@ -48,6 +48,9 @@ async def list_advertisements( search: Optional[str] = Query( None, description="Search in name tag, node name, or public key" ), + public_key: Optional[str] = Query( + None, description="Filter by source node public key" + ), observed_by: Optional[list[str]] = Query( None, description="Filter by receiver node public keys" ), @@ -97,6 +100,9 @@ async def list_advertisements( ) ) + if public_key: + query = query.where(Advertisement.public_key == public_key) + if observed_by: query = query.where(ObserverNode.public_key.in_(observed_by)) diff --git a/tests/test_api/test_advertisements.py b/tests/test_api/test_advertisements.py index 59414e0..c033da6 100644 --- a/tests/test_api/test_advertisements.py +++ b/tests/test_api/test_advertisements.py @@ -293,6 +293,52 @@ class TestListAdvertisementsFilters: data = response.json() assert len(data["items"]) == 1 + def test_filter_by_public_key(self, client_no_auth, api_db_session): + """Test filtering advertisements by source public_key.""" + now = datetime.now(timezone.utc) + ad_a = Advertisement( + public_key="pka" * 11, + name="AdAlpha", + adv_type="CLIENT", + received_at=now, + ) + ad_b = Advertisement( + public_key="pkb" * 11, + name="AdBeta", + adv_type="CLIENT", + received_at=now, + ) + api_db_session.add_all([ad_a, ad_b]) + api_db_session.commit() + + response = client_no_auth.get( + f"/api/v1/advertisements?public_key={ad_a.public_key}" + ) + assert response.status_code == 200 + data = response.json() + assert data["total"] == 1 + assert data["items"][0]["public_key"] == ad_a.public_key + + def test_filter_by_public_key_no_match(self, client_no_auth, api_db_session): + """Test filtering by public_key with no matching ads returns empty.""" + now = datetime.now(timezone.utc) + ad = Advertisement( + public_key="pkx" * 11, + name="AdX", + adv_type="CLIENT", + received_at=now, + ) + api_db_session.add(ad) + api_db_session.commit() + + response = client_no_auth.get( + "/api/v1/advertisements?public_key=nonexistent0000000000000000000" + ) + assert response.status_code == 200 + data = response.json() + assert data["total"] == 0 + assert data["items"] == [] + class TestAdvertisementSort: """Tests for advertisement list sort parameters."""