diff --git a/README.md b/README.md index ecddc4b..3bbb66e 100644 --- a/README.md +++ b/README.md @@ -250,4 +250,5 @@ Example response: - [ ] Add user information. Name, mail address - [ ] Add audit logging. Audit log entries will contain actor(userid+username), timestamp, action type (login, database change etc.), target outcome(successful, failed, or generating a warning), source information (ip address, device ID, and/or location from which the action orginated) and before/after state. - [ ] Add view for viewing audit log. -- [ ] Change messages in tables to display information about the entire messege, not only the payload. \ No newline at end of file +- [ ] Change messages in tables to display information about the entire messege, not only the payload. +- [X] Fix facet counts on Messages show 1 per device. diff --git a/api/app.py b/api/app.py index 68fc6af..3980f55 100644 --- a/api/app.py +++ b/api/app.py @@ -1472,6 +1472,38 @@ def create_app( count = row[0] if row is not None else 0 return {"count": count} + @app.get( + "/messages/facet-counts", tags=["meta"], dependencies=[Depends(authenticate)] + ) + def message_facet_counts() -> dict[str, Any]: + """Occurrence counts per IMEI and per device name over ALL messages + (not just a page), for the Messages view's filter popovers. Values + with zero messages still appear when a settings row exists (merged + from imei_map), so the facets list devices that "might occur".""" + try: + conn = psycopg.connect(config.dsn) + try: + with conn.cursor() as cur: + cur.execute( + "SELECT imei, count(*) FROM mqtt_messages GROUP BY imei" + ) + imei_counts = {row[0]: row[1] for row in cur.fetchall()} + cur.execute( + "SELECT device, count(*) FROM mqtt_messages GROUP BY device" + ) + device_counts = {row[0]: row[1] for row in cur.fetchall()} + # devices that exist but have no messages yet: count 0 + cur.execute("SELECT imei, meta->>'device' FROM imei_map") + for imei, name in cur.fetchall(): + imei_counts.setdefault(imei, 0) + if name: + device_counts.setdefault(name, 0) + finally: + conn.close() + except psycopg.Error as exc: + raise _db_unavailable(exc) from exc + return {"imei": imei_counts, "device": device_counts} + @app.get("/messages", tags=["data"], dependencies=[Depends(authenticate)]) def list_messages( imei: list[str] | None = Query( diff --git a/frontend/src/views/Messages.vue b/frontend/src/views/Messages.vue index 1e237b0..8736bd5 100644 --- a/frontend/src/views/Messages.vue +++ b/frontend/src/views/Messages.vue @@ -14,6 +14,11 @@ const limit = 25 // the full device population for the facet lists (not just the loaded page) const devices = ref([]) +// real occurrence counts per value, from GET /messages/facet-counts +const counts = ref<{ imei: Record; device: Record }>({ + imei: {}, + device: {}, +}) // server-side sort + filters (mirrored into query params on load) const sort = ref('') @@ -74,9 +79,13 @@ function sortState(name: string): '' | 'asc' | 'desc' { } function facetValues(kind: 'imei' | 'device'): { value: string; count: number }[] { - // values from the full device list ("might occur"), not just the loaded page - if (kind === 'imei') return devices.value.map((d) => ({ value: d.imei, count: 1 })) - return devices.value.map((d) => ({ value: d.device, count: 1 })) + // real occurrence counts; devices with zero messages keep count 0 so the + // list covers values that "might occur" + const map = counts.value[kind] + const entries = Object.entries(map) + // most-occurring first, then alphabetical + entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + return entries.map(([value, count]) => ({ value, count })) } function onCheck(kind: 'imei' | 'device', value: string, checked: boolean) { @@ -112,6 +121,14 @@ onMounted(async () => { } catch { // facet lists just stay empty; the text filters still work } + try { + counts.value = await api<{ + imei: Record + device: Record + }>('/api/messages/facet-counts') + } catch { + // counts stay zero; the facet lists still render from the devices + } })