Files
digiserver-v2/app/blueprints/main.py
T
ske087 84b5cd3d48 Dashboard: add Playlist Overview card and compact the sidebar
Restructure the dashboard into a two-column layout: a wide main column
holding the new Playlist Overview card, and a narrow (220px) right
sidebar with the compacted Quick Actions and Workflow Guide cards.

The Overview card answers "which playlist should I add media to, and
who will see it?" without opening each playlist. Per playlist it shows:
  - name, linked to that playlist's content manager
  - item count, total duration and current version
  - "N/M online" coverage, or a "no players" warning
  - one chip per assigned player, with an online/offline dot, linking
    to that player's manage page
  - an explicit empty state when no player is assigned

Players with no playlist are surfaced in a separate warning, since they
silently display nothing.

main.py builds this in a fixed number of queries: players are fetched
once and grouped by playlist_id, rather than calling
Playlist.players per row (which would be one query per playlist).

Layout notes:
- minmax(0, 1fr) on the main column so long names cannot widen the grid
- sidebar is sticky on tall screens and collapses below the main column
  under 900px, where the two small cards sit side by side
- sidebar cards used the global .card hover lift; suppressed for these

Also fixes a corrupted emoji (U+FFFD) in the System Status card.

Verified at 1600/1200/950/850/600px: no overflow, no button label
wrapping, and correct dark-mode colours.
2026-09-11 14:28:49 +03:00

111 lines
3.7 KiB
Python

"""
Main Blueprint - Dashboard and Home Routes
"""
from flask import Blueprint, render_template, redirect, url_for
from flask_login import login_required, current_user
from app.extensions import db, cache
from app.models.player import Player
from app.models.playlist import Playlist
from app.models.content import Content
from app.utils.logger import get_recent_logs
import os
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
@login_required
@cache.cached(timeout=60, unless=lambda: current_user.role != 'viewer')
def dashboard():
"""Main dashboard page"""
# Get statistics
total_players = Player.query.count()
total_playlists = Playlist.query.count()
total_content = Content.query.count()
# Calculate storage usage
upload_folder = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static', 'uploads')
storage_mb = 0
if os.path.exists(upload_folder):
for filename in os.listdir(upload_folder):
filepath = os.path.join(upload_folder, filename)
if os.path.isfile(filepath):
storage_mb += os.path.getsize(filepath)
storage_mb = round(storage_mb / (1024 * 1024), 2) # Convert to MB
server_logs = get_recent_logs(20)
# Per-playlist overview for the dashboard: which playlist holds what, and
# which players consume it. This answers "where should I add media / which
# playlist needs editing?" without opening each playlist.
#
# Players are fetched in a single query and grouped in Python rather than
# relying on Playlist.players per row, to avoid one query per playlist.
playlists = Playlist.query.order_by(Playlist.name).all()
players_by_playlist = {}
for player in Player.query.order_by(Player.name).all():
if player.playlist_id is not None:
players_by_playlist.setdefault(player.playlist_id, []).append(player)
playlist_overview = []
for playlist in playlists:
assigned = players_by_playlist.get(playlist.id, [])
playlist_overview.append({
'playlist': playlist,
'content_count': playlist.contents.count(),
'total_duration': playlist.total_duration,
'players': assigned,
'player_count': len(assigned),
'online_count': sum(1 for p in assigned if p.is_online),
})
# Players with no playlist produce no content on screen, so surface them.
unassigned_players = Player.query.filter(Player.playlist_id.is_(None))\
.order_by(Player.name).all()
return render_template(
'dashboard.html',
total_players=total_players,
total_playlists=total_playlists,
total_content=total_content,
storage_mb=storage_mb,
recent_logs=server_logs,
playlist_overview=playlist_overview,
unassigned_players=unassigned_players
)
@main_bp.route('/health')
def health():
"""Health check endpoint"""
from flask import jsonify
import os
try:
# Check database
db.session.execute(db.text('SELECT 1'))
# Check disk space
upload_folder = os.path.join(
main_bp.root_path or '.',
'static/uploads'
)
if os.path.exists(upload_folder):
stat = os.statvfs(upload_folder)
free_space_gb = (stat.f_bavail * stat.f_frsize) / (1024**3)
else:
free_space_gb = 0
return jsonify({
'status': 'healthy',
'database': 'ok',
'disk_space_gb': round(free_space_gb, 2)
}), 200
except Exception as e:
return jsonify({
'status': 'unhealthy',
'error': str(e)
}), 500