no docke on the horizont

This commit is contained in:
ske087
2026-07-01 23:07:44 +03:00
parent 2c91a5666e
commit 5762dd420d
5 changed files with 646 additions and 309 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"srvmonitor": true,
"itassets": true,
"networkview": true
}
+5 -5
View File
@@ -1,5 +1,5 @@
77016
77018
77020
77022
77024
18392
18395
18398
18401
18405
@@ -10,6 +10,7 @@
.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;
@@ -22,8 +23,8 @@
.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.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; }
@@ -37,23 +38,50 @@
.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;
}
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;
}
.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; }
.ssh-section {
background-color: #f8f9fa; padding: 1.5rem; border-radius: 4px; margin-bottom: 2rem;
/* Mode chooser cards */
.mode-chooser {
display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem;
}
body.dark-mode .ssh-section { background-color: #2d3748; }
@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;
@@ -64,141 +92,273 @@
.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;
}
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; }
.player-form-section { display: none; }
.player-form-section.active { display: block; }
/* 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 { background-color: #007bff; color: white; }
.btn-primary:hover:not(:disabled) { background-color: #0056b3; }
.btn-success { background-color: #28a745; color: white; }
.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: 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); } }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.row.full { grid-template-columns: 1fr; }
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
.hidden-field { display: none; }
.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: 900px; margin-top: 2rem;">
<h1>Add New Player with SSH Deployment</h1>
<p style="color: #6c757d; margin-bottom: 2rem;">
Create a new digital signage player with automatic code deployment via SSH
<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>
<div class="card">
<!-- SSH Connection Test Section -->
<!-- ───────────────────────────── 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;">
🔌 SSH Connection Setup
🔌 Step 1 — SSH Connection
</h3>
<p class="form-help">First, test SSH connection to the target host for player deployment</p>
<p class="form-help">
Test SSH connectivity to the target Linux host before proceeding.
</p>
<div class="row">
<div class="row2col">
<div class="form-group">
<label>Target Hostname/IP *</label>
<label>Target Hostname / IP *</label>
<input type="text" id="ssh_hostname" class="form-control"
placeholder="e.g., 192.168.1.100 or player.example.com">
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">
<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="row">
<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">
<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">
<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()">
<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;">
<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>
<!-- Player Information Form -->
<div id="player_form_section" class="player-form-section">
<!-- 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 to store credentials for deployment -->
<!-- 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" id="form_deploy_player" name="deploy_player" value="1">
<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>
<h3 class="section-header blue">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 for this player</small>
<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 of the player (optional)</small>
placeholder="e.g., Main Office Reception Area">
<small class="form-help">Physical location (optional)</small>
</div>
<h3 class="section-header green">Authentication</h3>
<p class="form-help" style="margin-bottom: 1rem;">
Quick Connect recommended for easy setup
</p>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" id="password" class="form-control"
<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 for quick setup</small>
<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">
@@ -207,122 +367,132 @@
</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>
<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>
<h4>🚀 What Happens Next</h4>
<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">
<li><strong>Player Creation:</strong> Player record created with Auth Code</li>
<li><strong>Code Deployment:</strong> Player code from Kiwy-Signage repository deployed to <span id="deploy_host_info">target host</span></li>
<li><strong>Installation:</strong> Installation scripts executed on remote host</li>
<li><strong>Configuration:</strong> Configure <code>app_config.json</code> with Auth Code provided</li>
<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" id="create_deploy_btn" class="btn btn-success" style="padding: 0.75rem 2rem;">
⚙️ Create & Deploy Player
<button type="submit" class="btn btn-success" style="padding: 0.75rem 2rem;">
⚙️ Create &amp; Deploy Player
</button>
<button type="button" class="btn btn-secondary" onclick="cancelForm()" style="padding: 0.75rem 2rem; margin-left: 1rem;">
<a href="{{ url_for('players.list') }}" class="btn btn-secondary"
style="padding: 0.75rem 2rem; margin-left: 0.5rem;">
Cancel
</button>
</a>
</div>
</form>
</div>
</div>
</div><!-- /panel_deploy -->
</div>
<script>
let sshConnectionVerified = false;
// ── 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;
const port = parseInt(document.getElementById('ssh_port').value) || 22;
if (!hostname || !username || !password) {
alert('Please fill in all SSH connection fields');
alert('Please fill in all SSH connection fields.');
return;
}
const btn = document.getElementById('test_ssh_btn');
btn.disabled = true;
btn.innerHTML = '<span class="loading-spinner"></span>Testing connection...';
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',
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({hostname, username, password, port})
body: JSON.stringify({hostname, username, password, port})
})
.then(r => r.json())
.then(data => {
sshConnectionVerified = data.success;
statusDiv.className = 'connection-status ' + (data.success ? 'success' : 'error');
statusDiv.innerHTML = `<strong>${data.success ? '✓ Connected!' : '✗ Connection Failed'}</strong><br>${data.message}`;
statusDiv.innerHTML = '<strong>' + (data.success ? '✓ Connected!' : '✗ Connection Failed')
+ '</strong><br>' + data.message;
if (data.success) {
// Disable SSH fields and store credentials
document.getElementById('ssh_hostname').disabled = true;
document.getElementById('ssh_username').disabled = true;
document.getElementById('ssh_password').disabled = true;
document.getElementById('ssh_port').disabled = true;
document.getElementById('test_ssh_btn').style.display = 'none';
// 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';
// Store credentials in hidden form fields
// 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;
document.getElementById('form_ssh_port').value = port;
// Show player form
document.getElementById('player_form_section').classList.add('active');
// 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.disabled = false;
btn.innerHTML = '✓ Test SSH Connection';
})
.catch(err => {
statusDiv.className = 'connection-status error';
statusDiv.innerHTML = `<strong>✗ Error:</strong> ${err.message}`;
btn.disabled = false;
statusDiv.innerHTML = '<strong>✗ Error:</strong> ' + err.message;
btn.disabled = false;
btn.innerHTML = '✓ Test SSH Connection';
});
}
function clearSSHForm() {
document.getElementById('ssh_hostname').value = '';
document.getElementById('ssh_username').value = '';
document.getElementById('ssh_password').value = '';
document.getElementById('ssh_port').value = '22';
document.getElementById('ssh_hostname').disabled = false;
document.getElementById('ssh_username').disabled = false;
document.getElementById('ssh_password').disabled = false;
document.getElementById('ssh_port').disabled = false;
document.getElementById('test_ssh_btn').style.display = 'inline-block';
['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('player_form_section').classList.remove('active');
document.getElementById('deploy_player_form_section').classList.remove('active');
document.getElementById('add_player_form').reset();
sshConnectionVerified = false;
}
function cancelForm() {
if (confirm('Are you sure? All changes will be lost.')) {
window.location.href = '{{ url_for("players.list") }}';
}
}
</script>
@@ -0,0 +1,329 @@
{% 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;
}
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; }
.ssh-section {
background-color: #f8f9fa; padding: 1.5rem; border-radius: 4px; margin-bottom: 2rem;
}
body.dark-mode .ssh-section { background-color: #2d3748; }
.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;
}
.player-form-section { display: none; }
.player-form-section.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;
}
.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); } }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.row.full { grid-template-columns: 1fr; }
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
.hidden-field { display: none; }
</style>
<div class="container" style="max-width: 900px; margin-top: 2rem;">
<h1>Add New Player with SSH Deployment</h1>
<p style="color: #6c757d; margin-bottom: 2rem;">
Create a new digital signage player with automatic code deployment via SSH
</p>
<div class="card">
<!-- SSH Connection Test Section -->
<div class="ssh-section">
<h3 class="section-header purple" style="margin-top: 0;">
🔌 SSH Connection Setup
</h3>
<p class="form-help">First, test SSH connection to the target host for player deployment</p>
<div class="row">
<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 or player.example.com">
<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="row">
<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>
<!-- Player Information Form -->
<div id="player_form_section" class="player-form-section">
<form method="POST" id="add_player_form">
<!-- Hidden SSH fields to store credentials for deployment -->
<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" id="form_deploy_player" name="deploy_player" value="1">
<h3 class="section-header blue">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 for this player</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;">
Quick Connect recommended for easy setup
</p>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" id="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 for quick setup</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 Creation:</strong> Player record created with Auth Code</li>
<li><strong>Code Deployment:</strong> Player code from Kiwy-Signage repository deployed to <span id="deploy_host_info">target host</span></li>
<li><strong>Installation:</strong> Installation scripts executed on remote host</li>
<li><strong>Configuration:</strong> Configure <code>app_config.json</code> with Auth Code provided</li>
</ol>
</div>
<div style="margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #ddd;">
<button type="submit" id="create_deploy_btn" class="btn btn-success" style="padding: 0.75rem 2rem;">
⚙️ Create & Deploy Player
</button>
<button type="button" class="btn btn-secondary" onclick="cancelForm()" style="padding: 0.75rem 2rem; margin-left: 1rem;">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
<script>
let sshConnectionVerified = false;
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');
btn.disabled = true;
btn.innerHTML = '<span class="loading-spinner"></span>Testing connection...';
const statusDiv = document.getElementById('connection_status');
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 => {
sshConnectionVerified = data.success;
statusDiv.className = 'connection-status ' + (data.success ? 'success' : 'error');
statusDiv.innerHTML = `<strong>${data.success ? '✓ Connected!' : '✗ Connection Failed'}</strong><br>${data.message}`;
if (data.success) {
// Disable SSH fields and store credentials
document.getElementById('ssh_hostname').disabled = true;
document.getElementById('ssh_username').disabled = true;
document.getElementById('ssh_password').disabled = true;
document.getElementById('ssh_port').disabled = true;
document.getElementById('test_ssh_btn').style.display = 'none';
document.getElementById('clear_ssh_btn').style.display = 'inline-block';
// Store credentials in 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;
// Show player form
document.getElementById('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() {
document.getElementById('ssh_hostname').value = '';
document.getElementById('ssh_username').value = '';
document.getElementById('ssh_password').value = '';
document.getElementById('ssh_port').value = '22';
document.getElementById('ssh_hostname').disabled = false;
document.getElementById('ssh_username').disabled = false;
document.getElementById('ssh_password').disabled = false;
document.getElementById('ssh_port').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('player_form_section').classList.remove('active');
document.getElementById('add_player_form').reset();
sshConnectionVerified = false;
}
function cancelForm() {
if (confirm('Are you sure? All changes will be lost.')) {
window.location.href = '{{ url_for("players.list") }}';
}
}
</script>
{% endblock %}
-167
View File
@@ -1,167 +0,0 @@
services:
# ── Portal (main dashboard & SSO) ────────────────────────────────────────────
portal:
build: ./portal
container_name: edp-portal
expose:
- "5001"
volumes:
- portal-data:/app/data
environment:
- FLASK_ENV=production
- PORT=5001
- SECRET_KEY=${PORTAL_SECRET_KEY:-change-this-portal-secret-in-production}
- PORTAL_JWT_SECRET=${PORTAL_JWT_SECRET:-change-this-jwt-secret-in-production}
- DATABASE_URL=sqlite:////app/data/portal.db
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@localhost}
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5001/health').read()"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
networks:
- edp-network
# ── DigiServer v2 ─────────────────────────────────────────────────────────────
digiserver-app:
build: ./digiserver-v2
container_name: edp-digiserver
expose:
- "5000"
volumes:
- ./digiserver-v2/data/instance:/app/instance
- ./digiserver-v2/data/uploads:/app/app/static/uploads
environment:
- FLASK_ENV=production
- SECRET_KEY=${DIGISERVER_SECRET_KEY:-change-digiserver-secret}
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
- PORTAL_JWT_SECRET=${PORTAL_JWT_SECRET:-change-this-jwt-secret-in-production}
restart: unless-stopped
healthcheck:
test: ["CMD", "sh", "-c", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:5000/', timeout=5)\" || true"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- edp-network
# ── IT Asset Management ───────────────────────────────────────────────────────
itassets-app:
build: ./IT_asset_management
container_name: edp-itassets
expose:
- "5000"
volumes:
- itassets-data:/app/data
- itassets-uploads:/app/uploads
- itassets-pdfs:/app/pdfs
- itassets-docs:/app/doc_templates
- itassets-docx:/app/docx_output
environment:
- FLASK_ENV=production
- PORT=5000
- SECRET_KEY=${ITASSETS_SECRET_KEY:-change-itassets-secret}
- SQLALCHEMY_DATABASE_URI=sqlite:////app/data/itassets.db
- PORTAL_JWT_SECRET=${PORTAL_JWT_SECRET:-change-this-jwt-secret-in-production}
- FLASK_APP=run.py
command: >
sh -c "flask db upgrade && gunicorn -b 0.0.0.0:5000 -w 4 --timeout 120 run:app"
restart: unless-stopped
networks:
- edp-network
# ── Server Monitor (Raspberry Pi monitoring + Ansible) ────────────────────────
srvmonitor-app:
build: ./Server_Monitorizare_v2
container_name: edp-srvmonitor
expose:
- "5000"
volumes:
- ./Server_Monitorizare_v2/data:/app/data
- ./Server_Monitorizare_v2/logs:/app/logs
- ./Server_Monitorizare_v2/ansible/inventory:/app/ansible/inventory
- ./Server_Monitorizare_v2/ansible/ssh_keys:/app/ansible/ssh_keys
environment:
- FLASK_ENV=production
- PORT=5000
- PORTAL_JWT_SECRET=${PORTAL_JWT_SECRET:-change-this-jwt-secret-in-production}
restart: unless-stopped
networks:
- edp-network
# ── NetworkView Backend (Node/Express + SQLite) ───────────────────────────────
networkview-backend:
build:
context: ./NetworkView/backend
dockerfile: Dockerfile
container_name: edp-networkview-backend
expose:
- "3001"
volumes:
- networkview-data:/data
environment:
- PORT=3001
- PORTAL_JWT_SECRET=${PORTAL_JWT_SECRET:-change-this-jwt-secret-in-production}
- NODE_ENV=production
restart: unless-stopped
networks:
- edp-network
# ── NetworkView Frontend (React/Vite SPA served by nginx) ────────────────────
networkview-frontend:
build:
context: ./NetworkView/frontend
dockerfile: Dockerfile
container_name: edp-networkview-frontend
expose:
- "80"
restart: unless-stopped
networks:
- edp-network
# ── Umbrella Nginx Reverse Proxy ──────────────────────────────────────────────
nginx:
image: nginx:alpine
container_name: edp-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./nginx/logs:/var/log/nginx
depends_on:
- portal
- digiserver-app
- itassets-app
- networkview-backend
- networkview-frontend
- srvmonitor-app
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:80/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
- edp-network
volumes:
portal-data:
itassets-data:
itassets-uploads:
itassets-pdfs:
itassets-docs:
itassets-docx:
networkview-data:
networks:
edp-network:
driver: bridge