Add room activity to stats view

This commit is contained in:
Jack Kingsman
2026-03-22 22:13:40 -07:00
parent da31b67d54
commit bb97b983bb
7 changed files with 86 additions and 1 deletions

View File

@@ -330,7 +330,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
</details>
### meshcore (2.3.1) — MIT
### meshcore (2.3.2) — MIT
<details>
<summary>Full license text</summary>

View File

@@ -833,4 +833,5 @@ class StatisticsResponse(BaseModel):
total_outgoing: int
contacts_heard: ContactActivityCounts
repeaters_heard: ContactActivityCounts
known_channels_active: ContactActivityCounts
path_hash_width_24h: PathHashWidthStats

View File

@@ -270,6 +270,30 @@ class StatisticsRepository:
"last_week": row["last_week"] or 0,
}
@staticmethod
async def _known_channels_active() -> dict[str, int]:
"""Count distinct known channel keys with channel traffic in each time window."""
now = int(time.time())
cursor = await db.conn.execute(
"""
SELECT
COUNT(DISTINCT CASE WHEN m.received_at >= ? THEN m.conversation_key END) AS last_hour,
COUNT(DISTINCT CASE WHEN m.received_at >= ? THEN m.conversation_key END) AS last_24_hours,
COUNT(DISTINCT CASE WHEN m.received_at >= ? THEN m.conversation_key END) AS last_week
FROM messages m
INNER JOIN channels c ON UPPER(m.conversation_key) = UPPER(c.key)
WHERE m.type = 'CHAN'
""",
(now - SECONDS_1H, now - SECONDS_24H, now - SECONDS_7D),
)
row = await cursor.fetchone()
assert row is not None
return {
"last_hour": row["last_hour"] or 0,
"last_24_hours": row["last_24_hours"] or 0,
"last_week": row["last_week"] or 0,
}
@staticmethod
async def _path_hash_width_24h() -> dict[str, int | float]:
"""Count parsed raw packets from the last 24h by hop hash width."""
@@ -396,6 +420,7 @@ class StatisticsRepository:
# Activity windows
contacts_heard = await StatisticsRepository._activity_counts(contact_type=2, exclude=True)
repeaters_heard = await StatisticsRepository._activity_counts(contact_type=2)
known_channels_active = await StatisticsRepository._known_channels_active()
path_hash_width_24h = await StatisticsRepository._path_hash_width_24h()
return {
@@ -411,5 +436,6 @@ class StatisticsRepository:
"total_outgoing": total_outgoing,
"contacts_heard": contacts_heard,
"repeaters_heard": repeaters_heard,
"known_channels_active": known_channels_active,
"path_hash_width_24h": path_hash_width_24h,
}

View File

@@ -164,6 +164,12 @@ export function SettingsStatisticsSection({ className }: { className?: string })
<td className="text-right py-1">{stats.repeaters_heard.last_24_hours}</td>
<td className="text-right py-1">{stats.repeaters_heard.last_week}</td>
</tr>
<tr>
<td className="py-1">Known-channels active</td>
<td className="text-right py-1">{stats.known_channels_active.last_hour}</td>
<td className="text-right py-1">{stats.known_channels_active.last_24_hours}</td>
<td className="text-right py-1">{stats.known_channels_active.last_week}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -584,6 +584,7 @@ describe('SettingsModal', () => {
total_outgoing: 30,
contacts_heard: { last_hour: 2, last_24_hours: 7, last_week: 10 },
repeaters_heard: { last_hour: 1, last_24_hours: 3, last_week: 3 },
known_channels_active: { last_hour: 1, last_24_hours: 4, last_week: 6 },
path_hash_width_24h: {
total_packets: 120,
single_byte: 60,
@@ -630,6 +631,7 @@ describe('SettingsModal', () => {
expect(screen.getByText('24 (20.0%)')).toBeInTheDocument();
expect(screen.getByText('Contacts heard')).toBeInTheDocument();
expect(screen.getByText('Repeaters heard')).toBeInTheDocument();
expect(screen.getByText('Known-channels active')).toBeInTheDocument();
// Busiest channels
expect(screen.getByText('general')).toBeInTheDocument();
@@ -650,6 +652,7 @@ describe('SettingsModal', () => {
total_outgoing: 30,
contacts_heard: { last_hour: 2, last_24_hours: 7, last_week: 10 },
repeaters_heard: { last_hour: 1, last_24_hours: 3, last_week: 3 },
known_channels_active: { last_hour: 1, last_24_hours: 4, last_week: 6 },
path_hash_width_24h: {
total_packets: 120,
single_byte: 60,

View File

@@ -514,6 +514,7 @@ export interface StatisticsResponse {
total_outgoing: number;
contacts_heard: ContactActivityCounts;
repeaters_heard: ContactActivityCounts;
known_channels_active: ContactActivityCounts;
path_hash_width_24h: {
total_packets: number;
single_byte: number;

View File

@@ -29,6 +29,9 @@ class TestStatisticsEmpty:
assert result["repeaters_heard"]["last_hour"] == 0
assert result["repeaters_heard"]["last_24_hours"] == 0
assert result["repeaters_heard"]["last_week"] == 0
assert result["known_channels_active"]["last_hour"] == 0
assert result["known_channels_active"]["last_24_hours"] == 0
assert result["known_channels_active"]["last_week"] == 0
assert result["path_hash_width_24h"] == {
"total_packets": 0,
"single_byte": 0,
@@ -256,6 +259,51 @@ class TestActivityWindows:
assert result["repeaters_heard"]["last_24_hours"] == 1
assert result["repeaters_heard"]["last_week"] == 1
@pytest.mark.asyncio
async def test_known_channels_active_windows(self, test_db):
"""Known channels are counted by distinct active keys in each time window."""
now = int(time.time())
conn = test_db.conn
known_1h = "AA" * 16
known_24h = "BB" * 16
known_7d = "CC" * 16
unknown_key = "DD" * 16
await conn.execute("INSERT INTO channels (key, name) VALUES (?, ?)", (known_1h, "chan-1h"))
await conn.execute(
"INSERT INTO channels (key, name) VALUES (?, ?)", (known_24h, "chan-24h")
)
await conn.execute("INSERT INTO channels (key, name) VALUES (?, ?)", (known_7d, "chan-7d"))
await conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES (?, ?, ?, ?)",
("CHAN", known_1h, "recent-1", now - 1200),
)
await conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES (?, ?, ?, ?)",
("CHAN", known_1h, "recent-2", now - 600),
)
await conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES (?, ?, ?, ?)",
("CHAN", known_24h, "day-old", now - 43200),
)
await conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES (?, ?, ?, ?)",
("CHAN", known_7d, "week-old", now - 259200),
)
await conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES (?, ?, ?, ?)",
("CHAN", unknown_key, "unknown", now - 600),
)
await conn.commit()
result = await StatisticsRepository.get_all()
assert result["known_channels_active"]["last_hour"] == 1
assert result["known_channels_active"]["last_24_hours"] == 2
assert result["known_channels_active"]["last_week"] == 3
class TestPathHashWidthStats:
@pytest.mark.asyncio