diff --git a/app/blueprints/main.py b/app/blueprints/main.py
index 322c377..7d9fd45 100644
--- a/app/blueprints/main.py
+++ b/app/blueprints/main.py
@@ -33,7 +33,20 @@ def dashboard():
storage_mb += os.path.getsize(filepath)
storage_mb = round(storage_mb / (1024 * 1024), 2) # Convert to MB
- server_logs = get_recent_logs(20)
+ # Recent Activity.
+ #
+ # Player feedback ("Feedback received from ...") is a ~15s heartbeat: it is
+ # ~84% of all log rows, and 18 of the newest 20. Showing it unfiltered
+ # buries the events an operator actually cares about (uploads, logins,
+ # HTTPS changes, deployments), so the two kinds are queried separately and
+ # the card defaults to real activity, with heartbeats available on demand.
+ #
+ # The split is done in SQL rather than by filtering one wide window in
+ # Python: with a fleet of players the heartbeat rate is high enough that a
+ # fixed window would return mostly noise and only a handful of real events.
+ HEARTBEAT_PREFIX = 'Feedback received from'
+ activity_logs = get_recent_logs(25, exclude_prefix=HEARTBEAT_PREFIX)
+ heartbeat_logs = get_recent_logs(25, include_prefix=HEARTBEAT_PREFIX)
# Per-playlist overview for the dashboard: which playlist holds what, and
# which players consume it. This answers "where should I add media / which
@@ -69,7 +82,8 @@ def dashboard():
total_playlists=total_playlists,
total_content=total_content,
storage_mb=storage_mb,
- recent_logs=server_logs,
+ recent_logs=activity_logs,
+ heartbeat_logs=heartbeat_logs,
playlist_overview=playlist_overview,
unassigned_players=unassigned_players
)
diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html
index e4840e7..4b78ff4 100644
--- a/app/templates/dashboard.html
+++ b/app/templates/dashboard.html
@@ -121,6 +121,57 @@
{% endif %}
+
+
+ {% if recent_logs or heartbeat_logs %}
+
+
+ Recent Activity
+
+
+
+
+
+
+
+ {% if recent_logs %}
+ {% for log in recent_logs %}
+
+
+
{% endblock %}
diff --git a/app/utils/logger.py b/app/utils/logger.py
index 4f8cc73..f9383b2 100644
--- a/app/utils/logger.py
+++ b/app/utils/logger.py
@@ -23,21 +23,42 @@ def log_action(level: str, message: str) -> None:
db.session.rollback()
-def get_recent_logs(limit: int = 20, level: Optional[str] = None) -> list:
+def get_recent_logs(limit: int = 20, level: Optional[str] = None,
+ exclude_prefix: Optional[str] = None,
+ include_prefix: Optional[str] = None) -> list:
"""Get the most recent log entries.
-
+
Args:
limit: Maximum number of logs to return
level: Optional filter by log level
-
+ exclude_prefix: Drop entries whose message starts with this text.
+ Useful for high-frequency noise: player feedback alone is ~84% of
+ all rows, so over-fetching a window and filtering in Python makes
+ the number of *useful* entries depend on heartbeat volume. Filtering
+ in SQL keeps a busy fleet from starving the result.
+ include_prefix: Keep only entries whose message starts with this text.
+
Returns:
- List of ServerLog instances
+ List of ServerLog instances, newest first
+
+ Note:
+ Prefixes are escaped before reaching SQL LIKE (via ``autoescape``), so a
+ literal '%' or '_' inside the prefix matches itself rather than acting
+ as a wildcard.
"""
query = ServerLog.query
if level:
query = query.filter_by(level=level)
+ if exclude_prefix:
+ query = query.filter(
+ ~ServerLog.message.startswith(exclude_prefix, autoescape=True))
+
+ if include_prefix:
+ query = query.filter(
+ ServerLog.message.startswith(include_prefix, autoescape=True))
+
return query.order_by(ServerLog.timestamp.desc()).limit(limit).all()