From b4c3f6563674ffb8363068575a5b47e993de1c80 Mon Sep 17 00:00:00 2001 From: scheianu Date: Fri, 11 Sep 2026 15:42:45 +0300 Subject: [PATCH] Dashboard: move Recent Activity under Playlist Overview and split out heartbeats Moves the Recent Activity card into the main column, directly beneath Playlist Overview, so the two cards that describe "what the server and its players are doing" sit together. System Status stays full-width below the grid. While moving it, the card was showing almost nothing useful: player feedback ("Feedback received from ...") is a ~15s heartbeat that makes up 84% of all log rows (423/506) and 18 of the newest 20, so the list was a wall of repeated one-line status reports that pushed real events off the screen. The card is now tabbed: - "Activity" (default) - uploads, logins, HTTPS changes, deployments... - "Player heartbeats" - the raw status stream, on demand get_recent_logs() gains exclude_prefix / include_prefix so the split happens in SQL. Filtering a single wide window in Python instead gave only 7 useful entries, because the number returned depends on heartbeat volume and would fall further as more players are added. Both queries now return 25. Prefixes are passed with autoescape=True so a literal '%' or '_' in a prefix matches itself rather than acting as a LIKE wildcard. UI: the list is capped at 320px and scrolls, so a long log cannot make the card dominate the page. Long messages wrap instead of pushing the timestamp off-screen (previously float:right), and the debug level now has its own colour instead of rendering as "info" green. Verified: 25/25 entries per tab, no cross-contamination, no horizontal overflow at 500-1400px, tabs wrap below the title under ~600px, and dark mode uses the theme variables. --- app/blueprints/main.py | 18 +++- app/templates/dashboard.html | 165 +++++++++++++++++++++++++++++++---- app/utils/logger.py | 29 +++++- 3 files changed, 188 insertions(+), 24 deletions(-) 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 %} +
+ + [{{ log.level.upper() }}] + + {{ log.message }} + {{ log.timestamp | localtime('%Y-%m-%d %H:%M:%S') }} +
+ {% endfor %} + {% else %} +

No activity recorded yet.

+ {% endif %} +
+ + +
+ {% endif %} @@ -347,29 +398,84 @@ body.dark-mode .player-chip.is-offline .status-dot { background: #718096; } color: var(--text-secondary); } +/* ── Recent Activity: tabbed list, scrollable so it cannot dominate ────── */ +.log-tabs { + display: flex; + gap: 0.25rem; + flex-shrink: 0; +} +.log-tab { + background: var(--bg-color); + color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: 999px; + padding: 0.25rem 0.7rem; + font-size: 0.78rem; + font-weight: 500; + cursor: pointer; + font-family: inherit; + display: inline-flex; + align-items: center; + gap: 0.35rem; + transition: all 0.2s; +} +.log-tab:hover { + border-color: var(--primary-color); + color: var(--text-color); +} +.log-tab.is-active { + background: var(--primary-gradient); + border-color: transparent; + color: #fff; +} +.log-count { + background: rgba(0, 0, 0, 0.12); + border-radius: 999px; + padding: 0 0.4rem; + font-size: 0.72rem; + line-height: 1.5; +} +.log-tab.is-active .log-count { + background: rgba(255, 255, 255, 0.25); +} + +/* Cap the height and scroll: a fixed number of rows stay readable. */ +.log-pane { + max-height: 320px; + overflow-y: auto; + margin-top: 0.5rem; +} .log-item { - padding: 0.5rem; + padding: 0.5rem 0.25rem; border-bottom: 1px solid var(--border-color); + font-size: 0.86rem; + line-height: 1.5; + display: flex; + align-items: baseline; + gap: 0.5rem; + flex-wrap: wrap; +} +.log-item:last-child { + border-bottom: none; +} +.log-level { + font-weight: 700; + flex-shrink: 0; +} +.log-message { + /* Long messages wrap instead of pushing the timestamp off-screen. */ + flex: 1 1 auto; + min-width: 0; + word-break: break-word; +} +.log-time { + margin-left: auto; + flex-shrink: 0; + font-variant-numeric: tabular-nums; + white-space: nowrap; } -{% if recent_logs %} -
-

Recent Activity

-
- {% for log in recent_logs %} -
- - [{{ log.level.upper() }}] - - {{ log.message }} - {{ log.timestamp | localtime('%Y-%m-%d %H:%M:%S') }} -
- {% endfor %} -
-
-{% endif %} -

System Status

✅ All systems operational

@@ -377,4 +483,27 @@ body.dark-mode .player-chip.is-offline .status-dot { background: #718096; }

🔄 Groups removed - Streamlined workflow

⚡ DigiServer v2.0

+ + {% 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()