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.
This commit is contained in:
+147
-18
@@ -121,6 +121,57 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity sits under Playlist Overview: both describe what
|
||||
the server and its players are actually doing. -->
|
||||
{% if recent_logs or heartbeat_logs %}
|
||||
<div class="card">
|
||||
<h2 style="display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;">
|
||||
<span>Recent Activity</span>
|
||||
<span class="log-tabs">
|
||||
<button type="button" class="log-tab is-active" data-log-tab="activity">
|
||||
Activity <span class="log-count">{{ recent_logs | length }}</span>
|
||||
</button>
|
||||
<button type="button" class="log-tab" data-log-tab="heartbeat">
|
||||
Player heartbeats <span class="log-count">{{ heartbeat_logs | length }}</span>
|
||||
</button>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div class="log-pane" data-log-pane="activity">
|
||||
{% if recent_logs %}
|
||||
{% for log in recent_logs %}
|
||||
<div class="log-item">
|
||||
<span class="log-level" style="color: {% if log.level == 'error' %}#e74c3c{% elif log.level == 'warning' %}#f39c12{% elif log.level == 'debug' %}#718096{% else %}#27ae60{% endif %};">
|
||||
[{{ log.level.upper() }}]
|
||||
</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
<small class="secondary-text log-time">{{ log.timestamp | localtime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="empty-hint">No activity recorded yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="log-pane" data-log-pane="heartbeat" hidden>
|
||||
{% if heartbeat_logs %}
|
||||
<p class="empty-hint" style="margin-top: 0;">
|
||||
Routine player status reports — newest first.
|
||||
</p>
|
||||
{% for log in heartbeat_logs %}
|
||||
<div class="log-item">
|
||||
<span class="log-level" style="color: #27ae60;">[{{ log.level.upper() }}]</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
<small class="secondary-text log-time">{{ log.timestamp | localtime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="empty-hint">No player heartbeats recorded yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ── Right sidebar: small, fixed-width helper cards ─────────────────── -->
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if recent_logs %}
|
||||
<div class="card">
|
||||
<h2>Recent Activity</h2>
|
||||
<div style="margin-top: 1rem;">
|
||||
{% for log in recent_logs %}
|
||||
<div class="log-item">
|
||||
<span style="color: {% if log.level == 'error' %}#e74c3c{% elif log.level == 'warning' %}#f39c12{% else %}#27ae60{% endif %}; font-weight: bold;">
|
||||
[{{ log.level.upper() }}]
|
||||
</span>
|
||||
{{ log.message }}
|
||||
<small class="secondary-text" style="float: right;">{{ log.timestamp | localtime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h2>System Status</h2>
|
||||
<p>✅ All systems operational</p>
|
||||
@@ -377,4 +483,27 @@ body.dark-mode .player-chip.is-offline .status-dot { background: #718096; }
|
||||
<p>🔄 Groups removed - Streamlined workflow</p>
|
||||
<p>⚡ DigiServer v2.0</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* Recent Activity tabs. Toggles the `hidden` attribute so the panes work even
|
||||
if this script is blocked — the activity pane is simply always shown. */
|
||||
(function () {
|
||||
var tabs = document.querySelectorAll('.log-tab');
|
||||
var panes = document.querySelectorAll('.log-pane');
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
var target = tab.getAttribute('data-log-tab');
|
||||
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.toggle('is-active', t === tab);
|
||||
});
|
||||
panes.forEach(function (p) {
|
||||
p.hidden = p.getAttribute('data-log-pane') !== target;
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user