diff --git a/meshview/lang/en.json b/meshview/lang/en.json index 18df79d..b0e77a9 100644 --- a/meshview/lang/en.json +++ b/meshview/lang/en.json @@ -22,47 +22,69 @@ } }, "chat": { + "chat_title": "Chats:", "replying_to": "Replying to:", "view_packet_details": "View packet details" }, "nodelist": { - "search_placeholder": "Search by name or ID...", - "all_roles": "All Roles", - "all_channels": "All Channels", - "all_hw_models": "All HW Models", - "all_firmware": "All Firmware", - "export_csv": "Export CSV", - "clear_filters": "Clear Filters", - "showing": "Showing", - "nodes": "nodes", - "short": "Short", - "long_name": "Long Name", - "hw_model": "HW Model", - "firmware": "Firmware", - "role": "Role", - "last_lat": "Last Latitude", - "last_long": "Last Longitude", - "channel": "Channel", - "last_update": "Last Update", - "loading_nodes": "Loading nodes...", - "no_nodes": "No nodes found", - "error_nodes": "Error loading nodes" + "search_placeholder": "Search by name or ID...", + "all_roles": "All Roles", + "all_channels": "All Channels", + "all_hw": "All HW Models", + "all_firmware": "All Firmware", + + "show_favorites": "⭐ Show Favorites", + "show_all": "⭐ Show All", + "export_csv": "Export CSV", + "clear_filters": "Clear Filters", + + "showing_nodes": "Showing", + "nodes_suffix": "nodes", + + "loading_nodes": "Loading nodes...", + "error_loading_nodes": "Error loading nodes", + "no_nodes_found": "No nodes found", + + "short_name": "Short", + "long_name": "Long Name", + "hw_model": "HW Model", + "firmware": "Firmware", + "role": "Role", + "last_lat": "Last Latitude", + "last_long": "Last Longitude", + "channel": "Channel", + "last_seen": "Last Seen", + "favorite": "Favorite", + + "time_just_now": "just now", + "time_min_ago": "min ago", + "time_hr_ago": "hr ago", + "time_day_ago": "day ago", + "time_days_ago": "days ago" }, + "net": { "number_of_checkins": "Number of Check-ins:", "view_packet_details": "View packet details", "view_all_packets_from_node": "View all packets from this node", "no_packets_found": "No packets found." }, + "map": { - "channel": "Channel:", - "model": "Model:", - "role": "Role:", + "show_routers_only": "Show Routers Only", + "share_view": "Share This View", + "reset_filters": "Reset Filters To Defaults", + "channel_label": "Channel:", + "model_label": "Model:", + "role_label": "Role:", "last_seen": "Last seen:", "firmware": "Firmware:", - "show_routers_only": "Show Routers Only", - "share_view": "Share This View" + "link_copied": "Link Copied!", + "legend_traceroute": "Ruta de traceroute (con flechas)", + "legend_neighbor": "Enlace de vecino" + }, + "stats": { "mesh_stats_summary": "Mesh Statistics - Summary (all available in Database)", @@ -82,21 +104,20 @@ "all_channels": "All Channels", "node_id": "Node ID" }, - "top": - { - "top_traffic_nodes": "Top Traffic Nodes (last 24 hours)", - "chart_description_1": "This chart shows a bell curve (normal distribution) based on the total \"Times Seen\" values for all nodes. It helps visualize how frequently nodes are heard, relative to the average.", - "chart_description_2": "This \"Times Seen\" value is the closest that we can get to Mesh utilization by node.", - "mean_label": "Mean:", - "stddev_label": "Standard Deviation:", + "top": { + "top_traffic_nodes": "Top Nodes Traffic", + "channel": "Channel", + "search": "Search", + "search_placeholder": "Search nodes...", "long_name": "Long Name", "short_name": "Short Name", - "channel": "Channel", - "packets_sent": "Packets Sent", - "times_seen": "Times Seen", - "seen_percent": "Seen % of Mean", - "no_nodes": "No top traffic nodes available." + "packets_sent": "Sent (24h)", + "times_seen": "Seen (24h)", + "avg_gateways": "Avg Gateways", + "showing_nodes": "Showing", + "nodes_suffix": "nodes" }, + "nodegraph": { "channel_label": "Channel:", @@ -131,11 +152,59 @@ "telemetry": "Telemetry", "trace_route": "Trace Route", "neighbor_info": "Neighbor Info", - "direct_to_mqtt": "direct to MQTT", "all": "All", "map": "Map", "graph": "Graph" - } + }, + "node": { + "specifications": "Specifications:", + "node_id": "Node ID:", + "long_name": "Long Name:", + "short_name": "Short Name:", + "hw_model": "Hardware Model:", + "firmware": "Firmware:", + "role": "Role:", + "channel": "Channel:", + "latitude": "Latitude:", + "longitude": "Longitude:", + "last_update": "Last Update:", + "battery_voltage": "Battery & Voltage", + "air_channel": "Air & Channel Utilization", + "environment": "Environment Metrics", + "neighbors_chart": "Neighbors (Signal-to-Noise)", + "expand": "Expand", + "export_csv": "Export CSV", + "time": "Time", + "packet_id": "Packet ID", + "from": "From", + "to": "To", + "port": "Port", + "direct_to_mqtt": "Direct to MQTT", + "all_broadcast": "All" + }, + "packet": { + "loading": "Loading packet information...", + "packet_id_label": "Packet ID", + "from_node": "From Node", + "to_node": "To Node", + "channel": "Channel", + "port": "Port", + "raw_payload": "Raw Payload", + "decoded_telemetry": "Decoded Telemetry", + "location": "Location", + "seen_by": "Seen By", + "gateway": "Gateway", + "rssi": "RSSI", + "snr": "SNR", + "hops": "Hop", + "time": "Time", + "packet_source": "Packet Source", + "distance": "Distance", + "node_id_short": "Node ID", + "all_broadcast": "All", + "direct_to_mqtt": "Direct to MQTT" + } + } \ No newline at end of file diff --git a/meshview/lang/es.json b/meshview/lang/es.json index e430b12..83980fb 100644 --- a/meshview/lang/es.json +++ b/meshview/lang/es.json @@ -1,16 +1,16 @@ { "base": { - "conversations": "Conversaciones", + "chat": "Conversaciones", "nodes": "Nodos", - "everything": "Mostrar Todo", - "graph": "Gráficos de la Malla", + "everything": "Mostrar todo", + "graphs": "Gráficos de la Malla", "net": "Red Semanal", "map": "Mapa en Vivo", "stats": "Estadísticas", "top": "Nodos con Mayor Tráfico", "footer": "Visita Meshview en Github.", - "node id": "ID de Nodo", - "go to node": "Ir al nodo", + "node_id": "ID de Nodo", + "go_to_node": "Ir al nodo", "all": "Todos", "portnum_options": { "1": "Mensaje de Texto", @@ -21,48 +21,65 @@ "71": "Información de Vecinos" } }, + "chat": { - "replying_to": "Respondiendo a:", - "view_packet_details": "Ver detalles del paquete" + "chat_title": "Conversaciones:", + "replying_to": "Respondiendo a:", + "view_packet_details": "Ver detalles del paquete" }, + "nodelist": { "search_placeholder": "Buscar por nombre o ID...", - "all_roles": "Todos los Roles", - "all_channels": "Todos los Canales", - "all_hw_models": "Todos los Modelos", - "all_firmware": "Todo el Firmware", + "all_roles": "Todos los roles", + "all_channels": "Todos los canales", + "all_hw": "Todos los modelos", + "all_firmware": "Todo el firmware", + "show_favorites": "⭐ Mostrar favoritos", + "show_all": "⭐ Mostrar todos", "export_csv": "Exportar CSV", - "clear_filters": "Limpiar Filtros", - "showing": "Mostrando", - "nodes": "nodos", - "short": "Corto", - "long_name": "Largo", - "hw_model": "Modelo", + "clear_filters": "Limpiar filtros", + "showing_nodes": "Mostrando", + "nodes_suffix": "nodos", + "loading_nodes": "Cargando nodos...", + "error_loading_nodes": "Error al cargar nodos", + "no_nodes_found": "No se encontraron nodos", + "short_name": "Corto", + "long_name": "Nombre largo", + "hw_model": "Modelo HW", "firmware": "Firmware", "role": "Rol", - "last_lat": "Última Latitud", - "last_long": "Última Longitud", + "last_lat": "Última latitud", + "last_long": "Última longitud", "channel": "Canal", - "last_update": "Última Actualización", - "loading_nodes": "Cargando nodos...", - "no_nodes": "No se encontraron nodos", - "error_nodes": "Error al cargar nodos" + "last_seen": "Última vez visto", + "favorite": "Favorito", + "time_just_now": "justo ahora", + "time_min_ago": "min atrás", + "time_hr_ago": "h atrás", + "time_day_ago": "día atrás", + "time_days_ago": "días atrás" }, + "net": { - "number_of_checkins": "Número de registros:", - "view_packet_details": "Ver detalles del paquete", - "view_all_packets_from_node": "Ver todos los paquetes de este nodo", - "no_packets_found": "No se encontraron paquetes." + "net_title": "Red Semanal:", + "total_messages": "Número de mensajes:", + "view_packet_details": "Más Detalles" }, + "map": { - "channel": "Canal:", - "model": "Modelo:", - "role": "Rol:", + "filter_routers_only": "Mostrar solo enrutadores", + "share_view": "Compartir esta vista", + "reset_filters": "Restablecer filtros", + "channel_label": "Canal:", + "model_label": "Modelo:", + "role_label": "Rol:", "last_seen": "Visto por última vez:", "firmware": "Firmware:", - "show_routers_only": "Mostrar solo enrutadores", - "share_view": "Compartir esta vista" + "link_copied": "¡Enlace copiado!", + "legend_traceroute": "Ruta de traceroute (flechas de dirección)", + "legend_neighbor": "Vínculo de vecinos" }, + "stats": { "mesh_stats_summary": "Estadísticas de la Malla - Resumen (completas en la base de datos)", "total_nodes": "Nodos Totales", @@ -80,22 +97,22 @@ "export_csv": "Exportar CSV", "all_channels": "Todos los Canales" }, + "top": { - "top_traffic_nodes": "Tráfico (últimas 24 horas)", - "chart_description_1": "Este gráfico muestra una curva normal (distribución normal) basada en el valor total de \"Veces Visto\" para todos los nodos. Ayuda a visualizar con qué frecuencia se detectan los nodos en relación con el promedio.", - "chart_description_2": "Este valor de \"Veces Visto\" es lo más aproximado que tenemos al nivel de uso de la malla por nodo.", - "mean_label": "Media:", - "stddev_label": "Desviación Estándar:", + "top_traffic_nodes": "Tráfico de Nodos (24h)", + "channel": "Canal", + "search": "Buscar", + "search_placeholder": "Buscar nodos...", "long_name": "Nombre Largo", "short_name": "Nombre Corto", - "channel": "Canal", - "packets_sent": "Paquetes Enviados", - "times_seen": "Veces Visto", - "seen_percent": "% Visto respecto a la Media", - "no_nodes": "No hay nodos con mayor tráfico disponibles." + "packets_sent": "Enviados (24h)", + "times_seen": "Visto (24h)", + "avg_gateways": "Promedio de Gateways", + "showing_nodes": "Mostrando", + "nodes_suffix": "nodos" }, - "nodegraph": - { + + "nodegraph": { "channel_label": "Canal:", "search_placeholder": "Buscar nodo...", "search_button": "Buscar", @@ -109,34 +126,68 @@ "unknown": "Desconocido", "node_not_found": "¡Nodo no encontrado en el canal actual!" }, - "firehose": - { - "live_feed": "📡 Flujo en Vivo", - "pause": "Pausar", - "resume": "Continuar", - "time": "Hora", - "packet_id": "ID del Paquete", - "from": "De", - "to": "Para", - "port": "Puerto", - "links": "Enlaces", - "unknown_app": "APLICACIÓN DESCONOCIDA", - "text_message": "Mensaje de Texto", - "position": "Posición", - "node_info": "Información del Nodo", - "routing": "Enrutamiento", - "administration": "Administración", - "waypoint": "Punto de Ruta", - "store_forward": "Almacenar y Reenviar", - "telemetry": "Telemetría", - "trace_route": "Rastreo de Ruta", - "neighbor_info": "Información de Vecinos", + "firehose": { + "live_feed": "📡 Flujo en vivo", + "pause": "Pausar", + "resume": "Reanudar", + "time": "Hora", + "packet_id": "ID de paquete", + "from": "De", + "to": "A", + "port": "Puerto", + "direct_to_mqtt": "Directo a MQTT", + "all_broadcast": "Todos" + }, - "direct_to_mqtt": "Directo a MQTT", - "all": "Todos", - "map": "Mapa", - "graph": "Gráfico" - } + "node": { + "specifications": "Especificaciones:", + "node_id": "ID de Nodo:", + "long_name": "Nombre Largo:", + "short_name": "Nombre Corto:", + "hw_model": "Modelo de Hardware:", + "firmware": "Firmware:", + "role": "Rol:", + "channel": "Canal:", + "latitude": "Latitud:", + "longitude": "Longitud:", + "last_update": "Última Actualización:", + "battery_voltage": "Batería y voltaje", + "air_channel": "Utilización del aire y del canal", + "environment": "Métricas Ambientales", + "neighbors_chart": "Vecinos (Relación Señal/Ruido)", + "expand": "Ampliar", + "export_csv": "Exportar CSV", + "time": "Hora", + "packet_id": "ID del Paquete", + "from": "De", + "to": "A", + "port": "Puerto", + "direct_to_mqtt": "Directo a MQTT", + "all_broadcast": "Todos" + }, + "packet": { + "loading": "Cargando información del paquete...", + "packet_id_label": "ID del Paquete", + "from_node": "De", + "to_node": "A", + "channel": "Canal", + "port": "Puerto", + "raw_payload": "Payload sin procesar", + "decoded_telemetry": "Telemetría Decodificada", + "location": "Ubicación", + "seen_by": "Visto por", + "gateway": "Gateway", + "rssi": "RSSI", + "snr": "SNR", + "hops": "Saltos", + "time": "Hora", + "packet_source": "Origen del Paquete", + "distance": "Distancia", + "node_id_short": "ID de Nodo", + "all_broadcast": "Todos", + "direct_to_mqtt": "Directo a MQTT", + "signal": "Señal" + } } diff --git a/meshview/store.py b/meshview/store.py index b50d7a7..db85028 100644 --- a/meshview/store.py +++ b/meshview/store.py @@ -1,6 +1,5 @@ from datetime import datetime, timedelta - -from sqlalchemy import and_, func, or_, select, text +from sqlalchemy import select, and_, or_, func, cast, Text from sqlalchemy.orm import lazyload from meshview import database, models @@ -27,18 +26,12 @@ async def get_fuzzy_nodes(query): async def get_packets( from_node_id=None, to_node_id=None, - node_id=None, # legacy: match either from OR to + node_id=None, # legacy portnum=None, after=None, - contains=None, # NEW: SQL-level substring match + contains=None, # substring search limit=50, ): - """ - SQLAlchemy 2.0 async ORM version. - Supports strict from/to/node filtering, substring payload search, - portnum, since, and limit. - """ - async with database.async_session() as session: stmt = select(models.Packet) conditions = [] @@ -51,36 +44,40 @@ async def get_packets( if to_node_id is not None: conditions.append(models.Packet.to_node_id == to_node_id) - # Legacy node ID filter: match either direction + # Legacy node_id (either direction) if node_id is not None: conditions.append( - or_(models.Packet.from_node_id == node_id, models.Packet.to_node_id == node_id) + or_( + models.Packet.from_node_id == node_id, + models.Packet.to_node_id == node_id, + ) ) # Port filter if portnum is not None: conditions.append(models.Packet.portnum == portnum) - # Timestamp filter + # Timestamp filter using microseconds if after is not None: conditions.append(models.Packet.import_time_us > after) - # Case-insensitive substring search on UTF-8 payload (stored as BLOB) + # Case-insensitive substring search on payload (BLOB → TEXT) if contains: - contains_lower = contains.lower() - conditions.append(func.lower(models.Packet.payload).like(f"%{contains_lower}%")) + contains_lower = f"%{contains.lower()}%" + payload_text = cast(models.Packet.payload, Text) + conditions.append(func.lower(payload_text).like(contains_lower)) - # Apply all conditions + # Apply WHERE conditions if conditions: stmt = stmt.where(and_(*conditions)) - # Order newest → oldest + # Order by newest first stmt = stmt.order_by(models.Packet.import_time_us.desc()) - # Apply limit + # Limit stmt = stmt.limit(limit) - # Execute query + # Run query result = await session.execute(stmt) return result.scalars().all() diff --git a/meshview/templates/chat.html b/meshview/templates/chat.html index bfba95b..bd719fb 100644 --- a/meshview/templates/chat.html +++ b/meshview/templates/chat.html @@ -53,8 +53,9 @@
| Short | -Long Name | -HW Model | -Firmware | -Role | -Last Latitude | -Last Longitude | -Channel | -Last Seen | -+ | Short | +Long Name | +HW Model | +Firmware | +Role | +Last Latitude | +Last Longitude | +Channel | +Last Seen | +|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Loading nodes... | |||||||||||||||||||
| + Loading nodes... + | +|||||||||||||||||||