mirror of
https://github.com/stellarshenson/stellars-jupyterhub-ds.git
synced 2026-03-08 06:00:29 +00:00
- Remove unused *.html from services/jupyterhub/templates/ - Keep only certs/ subdirectory in templates/ - Rename templates_enhanced to html_templates_enhanced - Update Dockerfile COPY paths
359 lines
11 KiB
HTML
359 lines
11 KiB
HTML
{% extends "page.html" %}
|
|
|
|
{% block main %}
|
|
<div class="container">
|
|
<h1>Broadcast Notifications</h1>
|
|
<p class="text-muted">Send notifications to all active JupyterLab servers</p>
|
|
|
|
<hr>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<form id="notification-form">
|
|
<div class="mb-3">
|
|
<label for="message" class="form-label">Message <span class="text-danger">*</span></label>
|
|
<textarea
|
|
class="form-control"
|
|
id="message"
|
|
name="message"
|
|
rows="4"
|
|
maxlength="140"
|
|
placeholder="Type your notification message here..."
|
|
required></textarea>
|
|
<div class="form-text">Maximum 140 characters. <span id="char-count">0/140</span></div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="variant" class="form-label">Notification Type <span class="text-danger">*</span></label>
|
|
<select class="form-select" id="variant" name="variant" required>
|
|
<option value="default">Default</option>
|
|
<option value="info" selected>Info (Blue)</option>
|
|
<option value="success">Success (Green)</option>
|
|
<option value="warning">Warning (Yellow)</option>
|
|
<option value="error">Error (Red)</option>
|
|
<option value="in-progress">In Progress</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3 form-check">
|
|
<input type="checkbox" class="form-check-input" id="autoClose" name="autoClose">
|
|
<label class="form-check-label" for="autoClose">
|
|
Auto-close notification (automatically dismiss after a few seconds)
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Recipients</label>
|
|
<div class="form-check">
|
|
<input type="checkbox" class="form-check-input" id="send-to-all" checked>
|
|
<label class="form-check-label" for="send-to-all">
|
|
<strong>Send to all active servers</strong>
|
|
</label>
|
|
</div>
|
|
|
|
<div id="server-selection" class="mt-2" style="display: none;">
|
|
<div class="d-flex gap-2 mb-2">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="select-all">Select All</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="deselect-all">Deselect All</button>
|
|
</div>
|
|
<div id="server-list" class="border rounded p-2" style="max-height: 200px; overflow-y: auto;">
|
|
<span class="text-muted">Loading active servers...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-lg" id="send-btn">
|
|
<i class="fa fa-paper-plane" aria-hidden="true"></i>
|
|
<span id="send-btn-text">Send to All Active Servers</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<!-- Results section (hidden by default) -->
|
|
<div id="results-section" style="display: none;" class="mt-4">
|
|
<h3>Broadcast Results</h3>
|
|
<div id="results-alert"></div>
|
|
|
|
<button
|
|
class="btn btn-secondary btn-sm mb-3"
|
|
type="button"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#details-table"
|
|
aria-expanded="false"
|
|
aria-controls="details-table"
|
|
id="toggle-details-btn">
|
|
Show Details
|
|
</button>
|
|
|
|
<div class="collapse" id="details-table">
|
|
<table class="table table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>Status</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="details-tbody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
require(["jquery"], function($) {
|
|
"use strict";
|
|
|
|
var activeServers = [];
|
|
|
|
// Fetch active servers on page load
|
|
$.ajax({
|
|
url: '{{ base_url }}api/notifications/active-servers',
|
|
method: 'GET',
|
|
headers: {
|
|
'X-XSRFToken': getCookie('_xsrf')
|
|
},
|
|
success: function(data) {
|
|
activeServers = data.servers || [];
|
|
renderServerList();
|
|
},
|
|
error: function() {
|
|
$('#server-list').html('<span class="text-danger">Failed to load servers</span>');
|
|
}
|
|
});
|
|
|
|
function renderServerList() {
|
|
var $list = $('#server-list');
|
|
if (activeServers.length === 0) {
|
|
$list.html('<span class="text-muted">No active servers</span>');
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
activeServers.forEach(function(server) {
|
|
html += '<div class="form-check">' +
|
|
'<input type="checkbox" class="form-check-input server-checkbox" ' +
|
|
'id="server-' + escapeHtml(server.username) + '" ' +
|
|
'value="' + escapeHtml(server.username) + '" checked>' +
|
|
'<label class="form-check-label" for="server-' + escapeHtml(server.username) + '">' +
|
|
escapeHtml(server.username) + '</label></div>';
|
|
});
|
|
$list.html(html);
|
|
|
|
// Bind change event for button text update
|
|
$('.server-checkbox').on('change', updateButtonText);
|
|
}
|
|
|
|
// Toggle server selection visibility
|
|
$('#send-to-all').on('change', function() {
|
|
$('#server-selection').toggle(!this.checked);
|
|
updateButtonText();
|
|
});
|
|
|
|
// Select All / Deselect All
|
|
$('#select-all').on('click', function() {
|
|
$('.server-checkbox').prop('checked', true);
|
|
updateButtonText();
|
|
});
|
|
|
|
$('#deselect-all').on('click', function() {
|
|
$('.server-checkbox').prop('checked', false);
|
|
updateButtonText();
|
|
});
|
|
|
|
function updateButtonText() {
|
|
var sendToAll = $('#send-to-all').is(':checked');
|
|
var selectedCount = $('.server-checkbox:checked').length;
|
|
var totalCount = activeServers.length;
|
|
|
|
if (sendToAll) {
|
|
$('#send-btn-text').text('Send to All Active Servers');
|
|
} else if (selectedCount === 0) {
|
|
$('#send-btn-text').text('Select Recipients');
|
|
} else if (selectedCount === totalCount) {
|
|
$('#send-btn-text').text('Send to All ' + totalCount + ' Server(s)');
|
|
} else {
|
|
$('#send-btn-text').text('Send to ' + selectedCount + ' Server(s)');
|
|
}
|
|
}
|
|
|
|
// Character counter
|
|
$('#message').on('input', function() {
|
|
var length = $(this).val().length;
|
|
$('#char-count').text(length + '/140');
|
|
});
|
|
|
|
// Form submission
|
|
$('#notification-form').on('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
// Get form data
|
|
var message = $('#message').val().trim();
|
|
var variant = $('#variant').val();
|
|
var autoClose = $('#autoClose').is(':checked');
|
|
var sendToAll = $('#send-to-all').is(':checked');
|
|
|
|
// Build recipients list
|
|
var recipients = null;
|
|
if (!sendToAll) {
|
|
recipients = [];
|
|
$('.server-checkbox:checked').each(function() {
|
|
recipients.push($(this).val());
|
|
});
|
|
|
|
if (recipients.length === 0) {
|
|
alert('Please select at least one recipient');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate
|
|
if (!message) {
|
|
alert('Please enter a message');
|
|
return;
|
|
}
|
|
|
|
if (message.length > 140) {
|
|
alert('Message cannot exceed 140 characters');
|
|
return;
|
|
}
|
|
|
|
// Disable button and show loading
|
|
var $sendBtn = $('#send-btn');
|
|
var originalHtml = $sendBtn.html();
|
|
$sendBtn.prop('disabled', true);
|
|
$sendBtn.html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Sending...');
|
|
|
|
// Hide previous results
|
|
$('#results-section').hide();
|
|
|
|
// Build request payload
|
|
var payload = {
|
|
message: message,
|
|
variant: variant,
|
|
autoClose: autoClose
|
|
};
|
|
if (recipients !== null) {
|
|
payload.recipients = recipients;
|
|
}
|
|
|
|
// Send request
|
|
$.ajax({
|
|
url: '{{ base_url }}api/notifications/broadcast',
|
|
method: 'POST',
|
|
contentType: 'application/json',
|
|
headers: {
|
|
'X-XSRFToken': getCookie('_xsrf')
|
|
},
|
|
data: JSON.stringify(payload),
|
|
success: function(data) {
|
|
displayResults(data);
|
|
// Clear form
|
|
$('#message').val('');
|
|
$('#char-count').text('0/140');
|
|
$('#autoClose').prop('checked', false);
|
|
},
|
|
error: function(xhr) {
|
|
var errorMsg = 'Failed to send notification';
|
|
if (xhr.responseJSON && xhr.responseJSON.message) {
|
|
errorMsg = xhr.responseJSON.message;
|
|
}
|
|
alert('Error: ' + errorMsg);
|
|
},
|
|
complete: function() {
|
|
// Re-enable button
|
|
$sendBtn.prop('disabled', false);
|
|
$sendBtn.html(originalHtml);
|
|
}
|
|
});
|
|
});
|
|
|
|
function displayResults(data) {
|
|
var total = data.total || 0;
|
|
var successful = data.successful || 0;
|
|
var failed = data.failed || 0;
|
|
var details = data.details || [];
|
|
|
|
// Show results section
|
|
$('#results-section').show();
|
|
|
|
// Build alert message
|
|
var alertClass = 'alert-success';
|
|
var alertIcon = '<i class="fa fa-check-circle"></i>';
|
|
var alertMessage = '';
|
|
|
|
if (total === 0) {
|
|
alertClass = 'alert-info';
|
|
alertIcon = '<i class="fa fa-info-circle"></i>';
|
|
alertMessage = 'No active servers found';
|
|
} else if (failed === 0) {
|
|
alertMessage = alertIcon + ' Notification sent successfully to all ' + total + ' server(s)';
|
|
} else if (successful === 0) {
|
|
alertClass = 'alert-danger';
|
|
alertIcon = '<i class="fa fa-times-circle"></i>';
|
|
alertMessage = alertIcon + ' Failed to send notification to all ' + total + ' server(s)';
|
|
} else {
|
|
alertClass = 'alert-warning';
|
|
alertIcon = '<i class="fa fa-exclamation-triangle"></i>';
|
|
alertMessage = alertIcon + ' Notification sent with partial success<br>' +
|
|
'<strong>Delivered:</strong> ' + successful + '/' + total + ' servers | ' +
|
|
'<strong>Failed:</strong> ' + failed + '/' + total + ' servers';
|
|
}
|
|
|
|
$('#results-alert').html('<div class="alert ' + alertClass + '">' + alertMessage + '</div>');
|
|
|
|
// Build details table
|
|
var tbody = $('#details-tbody');
|
|
tbody.empty();
|
|
|
|
if (details.length > 0) {
|
|
details.forEach(function(detail) {
|
|
var statusBadge = '';
|
|
var errorCell = '';
|
|
|
|
if (detail.status === 'success') {
|
|
statusBadge = '<span class="badge bg-success"><i class="fa fa-check"></i> Success</span>';
|
|
errorCell = '-';
|
|
} else {
|
|
statusBadge = '<span class="badge bg-danger"><i class="fa fa-times"></i> Failed</span>';
|
|
errorCell = detail.error || 'Unknown error';
|
|
}
|
|
|
|
var row = '<tr>' +
|
|
'<td>' + escapeHtml(detail.username) + '</td>' +
|
|
'<td>' + statusBadge + '</td>' +
|
|
'<td>' + escapeHtml(errorCell) + '</td>' +
|
|
'</tr>';
|
|
|
|
tbody.append(row);
|
|
});
|
|
|
|
// Update button text
|
|
$('#toggle-details-btn').text('Show Details (' + details.length + ' servers)');
|
|
}
|
|
}
|
|
|
|
function getCookie(name) {
|
|
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
if (match) return match[2];
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
var map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|