mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 08:43:36 +02:00
Improve frontend response time
This commit is contained in:
@@ -227,6 +227,20 @@ class BotConfig(BaseModel):
|
||||
code: str = Field(default="", description="Python code for this bot")
|
||||
|
||||
|
||||
class UnreadCounts(BaseModel):
|
||||
"""Aggregated unread counts, mention flags, and last message times for all conversations."""
|
||||
|
||||
counts: dict[str, int] = Field(
|
||||
default_factory=dict, description="Map of stateKey -> unread count"
|
||||
)
|
||||
mentions: dict[str, bool] = Field(
|
||||
default_factory=dict, description="Map of stateKey -> has mention"
|
||||
)
|
||||
last_message_times: dict[str, int] = Field(
|
||||
default_factory=dict, description="Map of stateKey -> last message timestamp"
|
||||
)
|
||||
|
||||
|
||||
class AppSettings(BaseModel):
|
||||
"""Application settings stored in the database."""
|
||||
|
||||
|
||||
@@ -550,6 +550,86 @@ class MessageRepository:
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def get_unread_counts(name: str | None = None) -> dict:
|
||||
"""Get unread message counts, mention flags, and last message times for all conversations.
|
||||
|
||||
Args:
|
||||
name: User's display name for @[name] mention detection. If None, mentions are skipped.
|
||||
|
||||
Returns:
|
||||
Dict with 'counts', 'mentions', and 'last_message_times' keys.
|
||||
"""
|
||||
counts: dict[str, int] = {}
|
||||
mention_flags: dict[str, bool] = {}
|
||||
last_message_times: dict[str, int] = {}
|
||||
|
||||
mention_pattern = f"%@[{name}]%" if name else None
|
||||
|
||||
# Channel unreads
|
||||
cursor = await db.conn.execute(
|
||||
"""
|
||||
SELECT m.conversation_key,
|
||||
COUNT(*) as unread_count,
|
||||
MAX(m.received_at) as last_message_time,
|
||||
SUM(CASE WHEN m.text LIKE ? THEN 1 ELSE 0 END) > 0 as has_mention
|
||||
FROM messages m
|
||||
JOIN channels c ON m.conversation_key = c.key
|
||||
WHERE m.type = 'CHAN' AND m.outgoing = 0
|
||||
AND m.received_at > COALESCE(c.last_read_at, 0)
|
||||
GROUP BY m.conversation_key
|
||||
""",
|
||||
(mention_pattern or "",),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
for row in rows:
|
||||
state_key = f"channel-{row['conversation_key']}"
|
||||
counts[state_key] = row["unread_count"]
|
||||
if mention_pattern and row["has_mention"]:
|
||||
mention_flags[state_key] = True
|
||||
|
||||
# Contact unreads
|
||||
cursor = await db.conn.execute(
|
||||
"""
|
||||
SELECT m.conversation_key,
|
||||
COUNT(*) as unread_count,
|
||||
MAX(m.received_at) as last_message_time,
|
||||
SUM(CASE WHEN m.text LIKE ? THEN 1 ELSE 0 END) > 0 as has_mention
|
||||
FROM messages m
|
||||
JOIN contacts ct ON m.conversation_key = ct.public_key
|
||||
WHERE m.type = 'PRIV' AND m.outgoing = 0
|
||||
AND m.received_at > COALESCE(ct.last_read_at, 0)
|
||||
GROUP BY m.conversation_key
|
||||
""",
|
||||
(mention_pattern or "",),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
for row in rows:
|
||||
state_key = f"contact-{row['conversation_key']}"
|
||||
counts[state_key] = row["unread_count"]
|
||||
if mention_pattern and row["has_mention"]:
|
||||
mention_flags[state_key] = True
|
||||
|
||||
# Last message times for all conversations (including read ones)
|
||||
cursor = await db.conn.execute(
|
||||
"""
|
||||
SELECT type, conversation_key, MAX(received_at) as last_message_time
|
||||
FROM messages
|
||||
GROUP BY type, conversation_key
|
||||
"""
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
for row in rows:
|
||||
prefix = "channel" if row["type"] == "CHAN" else "contact"
|
||||
state_key = f"{prefix}-{row['conversation_key']}"
|
||||
last_message_times[state_key] = row["last_message_time"]
|
||||
|
||||
return {
|
||||
"counts": counts,
|
||||
"mentions": mention_flags,
|
||||
"last_message_times": last_message_times,
|
||||
}
|
||||
|
||||
|
||||
class RawPacketRepository:
|
||||
@staticmethod
|
||||
|
||||
@@ -3,14 +3,29 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.database import db
|
||||
from app.models import UnreadCounts
|
||||
from app.repository import MessageRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/read-state", tags=["read-state"])
|
||||
|
||||
|
||||
@router.get("/unreads", response_model=UnreadCounts)
|
||||
async def get_unreads(
|
||||
name: str | None = Query(default=None, description="User's name for @mention detection"),
|
||||
) -> UnreadCounts:
|
||||
"""Get unread counts, mention flags, and last message times for all conversations.
|
||||
|
||||
Computes unread counts server-side using last_read_at timestamps on
|
||||
channels and contacts, avoiding the need to fetch bulk messages.
|
||||
"""
|
||||
data = await MessageRepository.get_unread_counts(name)
|
||||
return UnreadCounts(**data)
|
||||
|
||||
|
||||
@router.post("/mark-all-read")
|
||||
async def mark_all_read() -> dict:
|
||||
"""Mark all contacts and channels as read.
|
||||
|
||||
+7
-29
@@ -7,7 +7,7 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
|
||||
from app.config import settings
|
||||
from app.radio import radio_manager
|
||||
from app.repository import ChannelRepository, ContactRepository, RawPacketRepository
|
||||
from app.repository import RawPacketRepository
|
||||
from app.websocket import ws_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -16,12 +16,15 @@ router = APIRouter()
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket) -> None:
|
||||
"""WebSocket endpoint for real-time updates."""
|
||||
"""WebSocket endpoint for real-time updates.
|
||||
|
||||
Only sends health status on initial connect. Contacts and channels
|
||||
are fetched via REST endpoints for faster parallel loading.
|
||||
"""
|
||||
await ws_manager.connect(websocket)
|
||||
|
||||
# Send initial state
|
||||
# Send initial health status
|
||||
try:
|
||||
# Health status
|
||||
db_size_mb = 0.0
|
||||
try:
|
||||
db_size_bytes = os.path.getsize(settings.database_path)
|
||||
@@ -45,31 +48,6 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
||||
}
|
||||
await ws_manager.send_personal(websocket, "health", health_data)
|
||||
|
||||
# Contacts - fetch all by paginating until exhausted
|
||||
all_contacts = []
|
||||
chunk_size = 500
|
||||
offset = 0
|
||||
while True:
|
||||
chunk = await ContactRepository.get_all(limit=chunk_size, offset=offset)
|
||||
all_contacts.extend(chunk)
|
||||
if len(chunk) < chunk_size:
|
||||
break
|
||||
offset += chunk_size
|
||||
|
||||
await ws_manager.send_personal(
|
||||
websocket,
|
||||
"contacts",
|
||||
[c.model_dump() for c in all_contacts],
|
||||
)
|
||||
|
||||
# Channels
|
||||
channels = await ChannelRepository.get_all()
|
||||
await ws_manager.send_personal(
|
||||
websocket,
|
||||
"channels",
|
||||
[c.model_dump() for c in channels],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error sending initial state: %s", e)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user