Initial commit
This commit is contained in:
37
templates/admin/base.html
Normal file
37
templates/admin/base.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ title or "SyncPlayer Admin" }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('admin.dashboard') }}">SyncPlayer</a>
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link" href="{{ url_for('admin.displays_list') }}">Displays</a>
|
||||
<a class="nav-link" href="{{ url_for('admin.videos_list') }}">Videos</a>
|
||||
<a class="nav-link" href="{{ url_for('admin.idle_image_page') }}">Idle Image</a>
|
||||
<a class="nav-link" href="{{ url_for('admin.events_list') }}">Events</a>
|
||||
<a class="nav-link" href="{{ url_for('admin.event_logs') }}">Event Logs</a>
|
||||
<a class="nav-link" href="{{ url_for('admin.system_logs') }}">System Logs</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container-fluid p-3">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for cat, msg in messages %}
|
||||
<div class="alert alert-{{ cat }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<script src="/static/vendor/socket.io.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
74
templates/admin/dashboard.html
Normal file
74
templates/admin/dashboard.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Connected Displays</span>
|
||||
<small class="text-muted">live</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0" id="tbl-displays">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Public URL</th>
|
||||
<th>Status</th>
|
||||
<th>Last seen</th>
|
||||
<th>Latency</th>
|
||||
<th>Offset</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in displays %}
|
||||
<tr data-public-id="{{ d.public_id }}">
|
||||
<td>{{ d.id }}</td>
|
||||
<td>{{ d.name }}</td>
|
||||
<td><a href="{{ url_for('main.display_page', public_id=d.public_id) }}" target="_blank">/display/{{ d.public_id }}</a></td>
|
||||
<td class="st">{{ 'online' if d.is_online else 'offline' }}</td>
|
||||
<td class="ls">{{ d.last_seen or '' }}</td>
|
||||
<td class="lat"></td>
|
||||
<td class="off"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Active Event</div>
|
||||
<div class="card-body">
|
||||
<div id="active-event">
|
||||
{% if active %}
|
||||
<div>
|
||||
<strong>{{ active.name }}</strong> (#{{ active.event_id }})
|
||||
</div>
|
||||
<div>Start: <code>{{ active.start_time_ms }}</code></div>
|
||||
{% else %}
|
||||
<div class="text-muted">No active event</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Manual Trigger</div>
|
||||
<div class="card-body">
|
||||
{% for e in events %}
|
||||
<form method="post" action="{{ url_for('admin.event_trigger', event_id=e.id) }}" class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>{{ e.name }}</strong>
|
||||
<small class="text-muted">(#{{ e.id }})</small>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" type="submit">Trigger</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
templates/admin/display_form.html
Normal file
17
templates/admin/display_form.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<h4>{{ 'Edit' if display else 'New' }} Display</h4>
|
||||
<form method="post" class="mt-3" style="max-width: 560px;">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input class="form-control" name="name" value="{{ display.name if display else '' }}" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Public ID</label>
|
||||
<input class="form-control" name="public_id" value="{{ display.public_id if display else '' }}" required />
|
||||
<div class="form-text">URL will be /display/<public_id></div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<a class="btn btn-link" href="{{ url_for('admin.displays_list') }}">Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
28
templates/admin/displays.html
Normal file
28
templates/admin/displays.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="m-0">Displays</h4>
|
||||
<a class="btn btn-success" href="{{ url_for('admin.display_new') }}">New Display</a>
|
||||
</div>
|
||||
<table class="table table-striped table-sm">
|
||||
<thead><tr><th>ID</th><th>Name</th><th>Public ID</th><th>URL</th><th>Online</th><th>Last seen</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for d in displays %}
|
||||
<tr>
|
||||
<td>{{ d.id }}</td>
|
||||
<td>{{ d.name }}</td>
|
||||
<td><code>{{ d.public_id }}</code></td>
|
||||
<td><a href="{{ url_for('main.display_page', public_id=d.public_id) }}" target="_blank">/display/{{ d.public_id }}</a></td>
|
||||
<td>{{ 'yes' if d.is_online else 'no' }}</td>
|
||||
<td>{{ d.last_seen or '' }}</td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin.display_edit', display_id=d.id) }}">Edit</a>
|
||||
<form method="post" action="{{ url_for('admin.display_delete', display_id=d.id) }}" style="display:inline" onsubmit="return confirm('Delete display?');">
|
||||
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
53
templates/admin/event_form.html
Normal file
53
templates/admin/event_form.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<h4>{{ 'Edit' if event else 'New' }} Event</h4>
|
||||
<form method="post" class="mt-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-5">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input class="form-control" name="name" value="{{ event.name if event else '' }}" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">UDP Port (optional)</label>
|
||||
<input class="form-control" name="udp_port" value="{{ event.udp_port if event and event.udp_port else '' }}" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">UDP Payload (optional)</label>
|
||||
<input class="form-control" name="udp_payload" value="{{ event.udp_payload if event and event.udp_payload else '' }}" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Cooldown seconds</label>
|
||||
<input class="form-control" name="cooldown_seconds" value="{{ event.cooldown_seconds if event else 2 }}" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header">Display → Video Mappings</div>
|
||||
<div class="card-body">
|
||||
{% set existing = existing or {} %}
|
||||
{% for d in displays %}
|
||||
<div class="row align-items-center mb-2">
|
||||
<div class="col-4"><strong>{{ d.name }}</strong><br /><small class="text-muted"><code>{{ d.public_id }}</code></small></div>
|
||||
<div class="col-8">
|
||||
<select class="form-select form-select-sm" name="map_{{ d.id }}">
|
||||
<option value="">(none)</option>
|
||||
{% for v in videos %}
|
||||
<option value="{{ v.id }}" {% if existing.get(d.id) == v.id %}selected{% endif %}>{{ v.filename }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<a class="btn btn-link" href="{{ url_for('admin.events_list') }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
17
templates/admin/event_logs.html
Normal file
17
templates/admin/event_logs.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<h4>Event Logs</h4>
|
||||
<table class="table table-sm table-striped mt-3">
|
||||
<thead><tr><th>When</th><th>Event</th><th>Source</th><th>IP</th></tr></thead>
|
||||
<tbody>
|
||||
{% for l in logs %}
|
||||
<tr>
|
||||
<td>{{ l.triggered_at }}</td>
|
||||
<td>{{ l.event.name }} (#{{ l.event_id }})</td>
|
||||
<td>{{ l.trigger_source }}</td>
|
||||
<td>{{ l.source_ip or '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
45
templates/admin/events.html
Normal file
45
templates/admin/events.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="m-0">Events</h4>
|
||||
<a class="btn btn-success" href="{{ url_for('admin.event_new') }}">New Event</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info py-2">
|
||||
<div><strong>HTTP trigger</strong> (LAN): open one of these URLs</div>
|
||||
<div class="small">
|
||||
<code>{{ request.host_url }}trigger/<event_id></code> or <code>{{ request.host_url }}trigger_by_name/<event_name></code>
|
||||
(add <code>?force=1</code> to bypass cooldown)
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped table-sm">
|
||||
<thead><tr><th>ID</th><th>Name</th><th>UDP</th><th>Cooldown</th><th>Last triggered</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for e in events %}
|
||||
<tr>
|
||||
<td>{{ e.id }}</td>
|
||||
<td>
|
||||
{{ e.name }}
|
||||
</td>
|
||||
<td>{% if e.udp_port and e.udp_payload %}<code>{{ e.udp_port }}</code> / <code>{{ e.udp_payload }}</code>{% endif %}</td>
|
||||
<td>{{ e.cooldown_seconds }}s</td>
|
||||
<td>{{ e.last_triggered or '' }}</td>
|
||||
<td class="text-end">
|
||||
<form method="post" action="{{ url_for('admin.event_trigger', event_id=e.id) }}" style="display:inline">
|
||||
<button class="btn btn-outline-success btn-sm" type="submit">Trigger</button>
|
||||
</form>
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin.event_edit', event_id=e.id) }}">Edit</a>
|
||||
<form method="post" action="{{ url_for('admin.event_delete', event_id=e.id) }}" style="display:inline" onsubmit="return confirm('Delete event?');">
|
||||
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button>
|
||||
</form>
|
||||
|
||||
<div class="small text-muted mt-1">
|
||||
<div><code>/trigger/{{ e.id }}?force=1</code></div>
|
||||
<div><code>/trigger_by_name/{{ e.name|urlencode }}?force=1</code></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
42
templates/admin/idle_image.html
Normal file
42
templates/admin/idle_image.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="m-0">Idle Image</h4>
|
||||
</div>
|
||||
|
||||
<div class="row g-3" style="max-width: 900px;">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Current</div>
|
||||
<div class="card-body">
|
||||
{% if current_url %}
|
||||
<div class="mb-2"><code>{{ current }}</code></div>
|
||||
<div class="border" style="background:#111;">
|
||||
<img src="{{ current_url }}" alt="Idle image" style="display:block; width:100%; height:320px; object-fit:contain; background:#000;" />
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('admin.idle_image_clear') }}" class="mt-3" onsubmit="return confirm('Clear idle image?');">
|
||||
<button class="btn btn-outline-danger" type="submit">Clear</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-muted">No idle image configured.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Upload / Replace</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ url_for('admin.idle_image_upload') }}" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" name="file" accept="image/png,image/jpeg,image/webp" required />
|
||||
<div class="form-text">Shown when no video is actively playing. Recommended: 1920x1080 or 3840x2160.</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Upload</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
templates/admin/system_logs.html
Normal file
9
templates/admin/system_logs.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4 class="m-0">System Logs</h4>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('admin.system_logs') }}">Refresh</a>
|
||||
</div>
|
||||
<div class="text-muted mt-2"><small>{{ log_path }}</small></div>
|
||||
<pre class="mt-3 p-3 bg-dark text-light" style="height:70vh; overflow:auto; white-space:pre-wrap;">{% for line in lines %}{{ line }}{% endfor %}</pre>
|
||||
{% endblock %}
|
||||
26
templates/admin/videos.html
Normal file
26
templates/admin/videos.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="m-0">Videos</h4>
|
||||
<a class="btn btn-success" href="{{ url_for('admin.videos_upload') }}">Upload</a>
|
||||
</div>
|
||||
<table class="table table-striped table-sm">
|
||||
<thead><tr><th>ID</th><th>Filename</th><th>Duration</th><th>Uploaded</th><th>URL</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for v in videos %}
|
||||
<tr>
|
||||
<td>{{ v.id }}</td>
|
||||
<td>{{ v.filename }}</td>
|
||||
<td>{{ ('%.3fs'|format(v.duration)) if v.duration else '' }}</td>
|
||||
<td>{{ v.uploaded_at }}</td>
|
||||
<td><a href="{{ url_for('main.media', filename=v.filename) }}" target="_blank">/media/{{ v.filename }}</a></td>
|
||||
<td class="text-end">
|
||||
<form method="post" action="{{ url_for('admin.videos_delete', video_id=v.id) }}" style="display:inline" onsubmit="return confirm('Delete video?');">
|
||||
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
12
templates/admin/videos_upload.html
Normal file
12
templates/admin/videos_upload.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<h4>Upload Video</h4>
|
||||
<form method="post" enctype="multipart/form-data" class="mt-3" style="max-width: 640px;">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" name="file" accept="video/mp4,video/webm" required />
|
||||
<div class="form-text">MP4 (H264 baseline/main) recommended for SoC players.</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Upload</button>
|
||||
<a class="btn btn-link" href="{{ url_for('admin.videos_list') }}">Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user