Files
digiserver-v2/app/templates/players/add_player.html
T
ske087 2af04e1db3 feat: add weblink playlists, SSH player deployment, Caddy HTTPS, build player page
- Content model: add url column + is_weblink() for web page content items
- Player model: add deployment tracking fields (status, timestamps, message)
- Content blueprint: add add_weblink and add_weblink_to_playlist routes;
  weblinks auto-deleted when removed from playlist
- Players blueprint: add SSH deploy mode in add_player; weblink-aware playlist
- API blueprint: weblink URL served directly in playlist response;
  add /api/deploy/test-ssh and /api/deploy/player endpoints
- Admin blueprint: add build-player page (clone from Gitea, write base config);
  replace nginx status card with Caddy status on HTTPS config page
- caddy_manager: rewritten to generate proper HTTPS/internal-CA Caddyfiles
  and reload Caddy via admin API (/load) for live config updates
- ssh_deploy, background_tasks, player_build: new utils for SSH deployment
- background_tasks: push Flask app context into background thread so DB
  updates after deployment complete correctly
- ssh_deploy: robust install script detection with passwordless sudo injection
  (uses SSH credentials, cleaned up after install)
- Dockerfile: add git, sshpass, openssh-client, rsync
- docker-compose: switch nginx to Caddy on ports 80/443; add port 5000 for dev
- Templates: add_player deploy mode UI, weblink form in playlist/upload pages,
  build_player admin page, Caddy status on HTTPS config page
- Migrations: add_url_to_content, add_deployment_fields_to_player
- app.py: call db.create_all() on startup for schema bootstrap
- config.py: add PLAYER_CODE_DIR and PLAYER_REPO_URL settings
2026-07-13 16:46:19 +03:00

500 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Add Player - DigiServer v2{% endblock %}
{% block content %}
<style>
.form-group { margin-bottom: 1rem; }
.form-group label { font-weight: bold; display: block; margin-bottom: 0.5rem; }
body.dark-mode .form-group label { color: #e2e8f0; }
.form-control {
width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;
box-sizing: border-box;
}
body.dark-mode .form-control {
background: #1a202c; border-color: #4a5568; color: #e2e8f0;
}
body.dark-mode .form-control:focus { border-color: #7c3aed; outline: none; }
.form-help { color: #6c757d; font-size: 0.875rem; }
body.dark-mode .form-help { color: #718096; }
.section-header {
margin-top: 2rem; padding-bottom: 0.5rem; border-bottom: 2px solid;
}
.section-header.blue { border-color: #007bff; }
body.dark-mode .section-header.blue { border-color: #667eea; }
.section-header.green { border-color: #28a745; }
body.dark-mode .section-header.green { border-color: #48bb78; }
.section-header.yellow { border-color: #ffc107; }
body.dark-mode .section-header.yellow { border-color: #ecc94b; }
.section-header.purple { border-color: #9b59b6; }
body.dark-mode .section-header.purple { border-color: #b794f6; }
body.dark-mode h1, body.dark-mode h3, body.dark-mode h4 { color: #e2e8f0; }
body.dark-mode p { color: #a0aec0; }
.info-box {
background-color: #e7f3ff; border-left: 4px solid #007bff; padding: 1rem; margin: 2rem 0;
}
body.dark-mode .info-box { background-color: #1a365d; border-left-color: #667eea; }
.info-box h4 { margin-top: 0; color: #007bff; }
body.dark-mode .info-box h4 { color: #667eea; }
.info-box code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
body.dark-mode .info-box code { background: #2d3748; color: #e2e8f0; }
body.dark-mode small { color: #718096; }
/* Mode chooser cards */
.mode-chooser {
display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem;
}
@media (max-width: 700px) { .mode-chooser { grid-template-columns: 1fr; } }
.mode-card {
border: 2px solid #ddd; border-radius: 8px; padding: 1.5rem;
cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s;
background: #fff; text-align: center;
}
body.dark-mode .mode-card { background: #2d3748; border-color: #4a5568; }
.mode-card:hover { border-color: #007bff; box-shadow: 0 4px 12px rgba(0,123,255,0.15); }
body.dark-mode .mode-card:hover { border-color: #667eea; }
.mode-card.active { border-color: #007bff; box-shadow: 0 4px 12px rgba(0,123,255,0.2); }
body.dark-mode .mode-card.active { border-color: #667eea; box-shadow: 0 4px 12px rgba(102,126,234,0.25); }
.mode-card.active-deploy { border-color: #28a745; box-shadow: 0 4px 12px rgba(40,167,69,0.2); }
body.dark-mode .mode-card.active-deploy { border-color: #48bb78; }
.mode-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.mode-title { font-size: 1.15rem; font-weight: bold; margin-bottom: 0.4rem; }
body.dark-mode .mode-title { color: #e2e8f0; }
.mode-desc { font-size: 0.875rem; color: #6c757d; }
body.dark-mode .mode-desc { color: #a0aec0; }
/* Panel visibility */
.mode-panel { display: none; }
.mode-panel.active { display: block; }
/* SSH section */
.ssh-section {
background-color: #f8f9fa; padding: 1.5rem; border-radius: 6px; margin-bottom: 2rem;
}
body.dark-mode .ssh-section { background-color: #1e2a38; }
.connection-status {
padding: 1rem; border-radius: 4px; margin-top: 1rem; display: none;
}
.connection-status.success {
background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; display: block;
}
.connection-status.error {
background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; display: block;
}
body.dark-mode .connection-status.success { background-color: #22543d; border-color: #2f855a; color: #9ae6b4; }
body.dark-mode .connection-status.error { background-color: #742a2a; border-color: #c53030; color: #fc8181; }
/* Deploy-form hidden until SSH verified */
.deploy-player-form { display: none; }
.deploy-player-form.active { display: block; }
.btn {
padding: 0.5rem 1rem; margin-right: 0.5rem; margin-bottom: 0.5rem;
border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem;
text-decoration: none; display: inline-block;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background-color: #007bff; color: white; }
.btn-primary:hover:not(:disabled) { background-color: #0056b3; }
.btn-success { background-color: #28a745; color: white; }
.btn-success:hover:not(:disabled) { background-color: #218838; }
.btn-secondary { background-color: #6c757d; color: white; }
.btn-secondary:hover:not(:disabled) { background-color: #5a6268; }
.loading-spinner {
display: inline-block; width: 1rem; height: 1rem;
border: 2px solid rgba(255,255,255,0.3); border-radius: 50%;
border-top-color: white; animation: spin 1s linear infinite; margin-right: 0.5rem;
}
@keyframes spin { to { transform: rotate(360deg); } }
.row2col { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 768px) { .row2col { grid-template-columns: 1fr; } }
</style>
<div class="container" style="max-width: 960px; margin-top: 2rem;">
<h1>Add New Player</h1>
<p style="color: #6c757d; margin-bottom: 1.5rem;">
Choose how you want to add the player to the system.
</p>
<!-- ───────────────────────────── Mode selector ───────────────────────────── -->
<div class="mode-chooser">
<div class="mode-card active" id="card_manual" onclick="selectMode('manual')">
<div class="mode-icon">📋</div>
<div class="mode-title">Manual Registration</div>
<div class="mode-desc">
Register the player in the database and wait for the client to connect.
You configure <code>app_config.json</code> on the device yourself.
</div>
</div>
<div class="mode-card" id="card_deploy" onclick="selectMode('deploy')">
<div class="mode-icon">🚀</div>
<div class="mode-title">Create &amp; Deploy</div>
<div class="mode-desc">
Register the player <em>and</em> automatically push the Linux player
code to the target host via SSH.
</div>
</div>
</div>
<!-- ─────────────────────── Mode 1 — Manual Registration ─────────────────── -->
<div class="card mode-panel active" id="panel_manual">
<form method="POST">
<h3 class="section-header blue" style="margin-top: 0;">Basic Information</h3>
<div class="form-group">
<label>Display Name *</label>
<input type="text" name="name" required class="form-control"
placeholder="e.g., Office Reception Player">
<small class="form-help">Friendly name for the player</small>
</div>
<div class="form-group">
<label>Hostname *</label>
<input type="text" name="hostname" required class="form-control"
placeholder="e.g., office-player-001">
<small class="form-help">
Unique identifier — must match <code>screen_name</code> in the player's
<code>app_config.json</code>
</small>
</div>
<div class="form-group">
<label>Location</label>
<input type="text" name="location" class="form-control"
placeholder="e.g., Main Office Reception Area">
<small class="form-help">Physical location of the player (optional)</small>
</div>
<h3 class="section-header green">Authentication</h3>
<p class="form-help" style="margin-bottom: 1rem;">
Choose one authentication method (Quick Connect recommended for easy setup)
</p>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" class="form-control"
placeholder="Leave empty to use Quick Connect only">
<small class="form-help">Secure password for player authentication (optional if using Quick Connect)</small>
</div>
<div class="form-group">
<label>Quick Connect Code *</label>
<input type="text" name="quickconnect_code" required class="form-control"
placeholder="e.g., OFFICE123">
<small class="form-help">
Easy pairing code — must match <code>quickconnect_key</code> in player config
</small>
</div>
<h3 class="section-header yellow">Display Settings</h3>
<div class="form-group">
<label>Orientation</label>
<select name="orientation" class="form-control">
<option value="Landscape" selected>Landscape</option>
<option value="Portrait">Portrait</option>
</select>
<small class="form-help">Display orientation for the player</small>
</div>
<div class="form-group">
<label>Assign Playlist</label>
<select name="playlist_id" class="form-control">
<option value="">No Playlist (Unassigned)</option>
{% for playlist in playlists %}
<option value="{{ playlist.id }}">
{{ playlist.name }} ({{ playlist.orientation }}) {{ playlist.content_count }} items
</option>
{% endfor %}
</select>
<small class="form-help">Assign player to a playlist (optional)</small>
</div>
<div class="info-box">
<h4>📋 Setup Instructions</h4>
<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">
<li>Create the player with the form above.</li>
<li>Note the generated <strong>Auth Code</strong> shown after creation.</li>
<li>Configure the player's <code>app_config.json</code>:
<ul style="margin-top: 0.5rem;">
<li><code>server_ip</code> — your server address</li>
<li><code>screen_name</code> — same as <strong>Hostname</strong> above</li>
<li><code>quickconnect_key</code> — same as <strong>Quick Connect Code</strong> above</li>
</ul>
</li>
<li>Start the player — it will authenticate automatically.</li>
</ol>
</div>
<div style="margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #ddd;">
<button type="submit" class="btn btn-success" style="padding: 0.75rem 2rem;">
✓ Create Player
</button>
<a href="{{ url_for('players.list') }}" class="btn btn-secondary"
style="padding: 0.75rem 2rem; margin-left: 0.5rem;">
Cancel
</a>
</div>
</form>
</div>
<!-- ──────────────────────── Mode 2 — Create & Deploy ────────────────────── -->
<div class="card mode-panel" id="panel_deploy">
<!-- Step 1 — SSH connection test -->
<div class="ssh-section">
<h3 class="section-header purple" style="margin-top: 0;">
🔌 Step 1 — SSH Connection
</h3>
<p class="form-help">
Test SSH connectivity to the target Linux host before proceeding.
</p>
<div class="row2col">
<div class="form-group">
<label>Target Hostname / IP *</label>
<input type="text" id="ssh_hostname" class="form-control"
placeholder="e.g., 192.168.1.100">
<small class="form-help">IP address or hostname of the target machine</small>
</div>
<div class="form-group">
<label>SSH Port</label>
<input type="number" id="ssh_port" class="form-control"
value="22" min="1" max="65535">
<small class="form-help">SSH port (default: 22)</small>
</div>
</div>
<div class="row2col">
<div class="form-group">
<label>SSH Username *</label>
<input type="text" id="ssh_username" class="form-control"
placeholder="e.g., pi or ubuntu">
<small class="form-help">SSH login username</small>
</div>
<div class="form-group">
<label>SSH Password *</label>
<input type="password" id="ssh_password" class="form-control"
placeholder="SSH password">
<small class="form-help">SSH login password</small>
</div>
</div>
<button type="button" id="test_ssh_btn" class="btn btn-primary"
onclick="testSSHConnection()">
✓ Test SSH Connection
</button>
<button type="button" id="clear_ssh_btn" class="btn btn-secondary"
onclick="clearSSHForm()" style="display: none;">
🔄 Clear
</button>
<div id="connection_status" class="connection-status"></div>
</div>
<!-- Step 2 — Player info (shown after successful SSH test) -->
<div id="deploy_player_form_section" class="deploy-player-form">
<form method="POST" id="add_player_form">
<!-- Hidden SSH fields -->
<input type="hidden" id="form_ssh_hostname" name="ssh_hostname" value="">
<input type="hidden" id="form_ssh_username" name="ssh_username" value="">
<input type="hidden" id="form_ssh_password" name="ssh_password" value="">
<input type="hidden" id="form_ssh_port" name="ssh_port" value="">
<input type="hidden" name="deploy_player" value="1">
<h3 class="section-header blue">Step 2 — Basic Information</h3>
<div class="form-group">
<label>Display Name *</label>
<input type="text" name="name" required class="form-control"
placeholder="e.g., Office Reception Player">
<small class="form-help">Friendly name for the player</small>
</div>
<div class="form-group">
<label>Hostname *</label>
<input type="text" name="hostname" required class="form-control"
placeholder="e.g., office-player-001">
<small class="form-help">
Unique identifier — will be written to <code>app_config.json</code>
on the remote host automatically
</small>
</div>
<div class="form-group">
<label>Location</label>
<input type="text" name="location" class="form-control"
placeholder="e.g., Main Office Reception Area">
<small class="form-help">Physical location (optional)</small>
</div>
<h3 class="section-header green">Authentication</h3>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" class="form-control"
placeholder="Leave empty to use Quick Connect only">
<small class="form-help">Secure password (optional if using Quick Connect)</small>
</div>
<div class="form-group">
<label>Quick Connect Code *</label>
<input type="text" name="quickconnect_code" required class="form-control"
placeholder="e.g., OFFICE123">
<small class="form-help">Easy pairing code — deployed automatically to remote config</small>
</div>
<h3 class="section-header yellow">Display Settings</h3>
<div class="form-group">
<label>Orientation</label>
<select name="orientation" class="form-control">
<option value="Landscape" selected>Landscape</option>
<option value="Portrait">Portrait</option>
</select>
<small class="form-help">Display orientation for the player</small>
</div>
<div class="form-group">
<label>Assign Playlist</label>
<select name="playlist_id" class="form-control">
<option value="">No Playlist (Unassigned)</option>
{% for playlist in playlists %}
<option value="{{ playlist.id }}">
{{ playlist.name }} ({{ playlist.orientation }}) {{ playlist.content_count }} items
</option>
{% endfor %}
</select>
<small class="form-help">Assign player to a playlist (optional)</small>
</div>
<div class="info-box">
<h4>🚀 What Happens Next</h4>
<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">
<li><strong>Player Record</strong> is created in the database with an Auth Code.</li>
<li><strong>Player Code</strong> from the Kiwy-Signage repository is pushed to
<strong id="deploy_host_info">the target host</strong> via SSH.</li>
<li><strong>Installation Scripts</strong> run on the remote host in the background.</li>
<li><strong>Config</strong> (<code>app_config.json</code>) is written automatically
with the server address, hostname and Quick Connect Code.</li>
</ol>
</div>
<div style="margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #ddd;">
<button type="submit" class="btn btn-success" style="padding: 0.75rem 2rem;">
⚙️ Create &amp; Deploy Player
</button>
<a href="{{ url_for('players.list') }}" class="btn btn-secondary"
style="padding: 0.75rem 2rem; margin-left: 0.5rem;">
Cancel
</a>
</div>
</form>
</div>
</div><!-- /panel_deploy -->
</div>
<script>
// ── Mode switching ─────────────────────────────────────────────────────────
function selectMode(mode) {
// cards
document.getElementById('card_manual').classList.remove('active', 'active-deploy');
document.getElementById('card_deploy').classList.remove('active', 'active-deploy');
if (mode === 'manual') {
document.getElementById('card_manual').classList.add('active');
} else {
document.getElementById('card_deploy').classList.add('active-deploy');
}
// panels
document.getElementById('panel_manual').classList.toggle('active', mode === 'manual');
document.getElementById('panel_deploy').classList.toggle('active', mode === 'deploy');
}
// ── SSH flow ───────────────────────────────────────────────────────────────
function testSSHConnection() {
const hostname = document.getElementById('ssh_hostname').value.trim();
const username = document.getElementById('ssh_username').value.trim();
const password = document.getElementById('ssh_password').value.trim();
const port = parseInt(document.getElementById('ssh_port').value) || 22;
if (!hostname || !username || !password) {
alert('Please fill in all SSH connection fields.');
return;
}
const btn = document.getElementById('test_ssh_btn');
const statusDiv = document.getElementById('connection_status');
btn.disabled = true;
btn.innerHTML = '<span class="loading-spinner"></span>Testing connection…';
fetch('{{ url_for("api.test_ssh_connection") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({hostname, username, password, port})
})
.then(r => r.json())
.then(data => {
statusDiv.className = 'connection-status ' + (data.success ? 'success' : 'error');
statusDiv.innerHTML = '<strong>' + (data.success ? '✓ Connected!' : '✗ Connection Failed')
+ '</strong><br>' + data.message;
if (data.success) {
// Lock SSH fields
['ssh_hostname','ssh_username','ssh_password','ssh_port'].forEach(id => {
document.getElementById(id).disabled = true;
});
document.getElementById('test_ssh_btn').style.display = 'none';
document.getElementById('clear_ssh_btn').style.display = 'inline-block';
// Copy credentials to hidden form fields
document.getElementById('form_ssh_hostname').value = hostname;
document.getElementById('form_ssh_username').value = username;
document.getElementById('form_ssh_password').value = password;
document.getElementById('form_ssh_port').value = port;
// Reveal player form and update host label
document.getElementById('deploy_player_form_section').classList.add('active');
document.getElementById('deploy_host_info').textContent = hostname;
}
btn.disabled = false;
btn.innerHTML = '✓ Test SSH Connection';
})
.catch(err => {
statusDiv.className = 'connection-status error';
statusDiv.innerHTML = '<strong>✗ Error:</strong> ' + err.message;
btn.disabled = false;
btn.innerHTML = '✓ Test SSH Connection';
});
}
function clearSSHForm() {
['ssh_hostname','ssh_username','ssh_password','ssh_port'].forEach(id => {
const el = document.getElementById(id);
el.value = id === 'ssh_port' ? '22' : '';
el.disabled = false;
});
document.getElementById('test_ssh_btn').style.display = 'inline-block';
document.getElementById('clear_ssh_btn').style.display = 'none';
document.getElementById('connection_status').className = 'connection-status';
document.getElementById('deploy_player_form_section').classList.remove('active');
document.getElementById('add_player_form').reset();
}
</script>
{% endblock %}