diff --git a/nebula/frontend/app.py b/nebula/frontend/app.py index c9e372c04..4018dbed0 100755 --- a/nebula/frontend/app.py +++ b/nebula/frontend/app.py @@ -164,19 +164,37 @@ async def connect(self, websocket: WebSocket): pass def disconnect(self, websocket: WebSocket): - self.active_connections.remove(websocket) + if websocket in self.active_connections: + self.active_connections.remove(websocket) def add_message(self, message): current_timestamp = datetime.datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d %H:%M:%S") self.historic_messages.update({current_timestamp: json.loads(message)}) async def send_personal_message(self, message: str, websocket: WebSocket): - await websocket.send_text(message) + try: + await websocket.send_text(message) + except RuntimeError: + # Connection was closed, remove it from active connections + self.disconnect(websocket) async def broadcast(self, message: str): self.add_message(message) + disconnected_websockets = [] + for connection in self.active_connections: - await connection.send_text(message) + try: + await connection.send_text(message) + except RuntimeError: + # Mark connection for removal + disconnected_websockets.append(connection) + except Exception as e: + logging.error(f"Error broadcasting message: {e}") + disconnected_websockets.append(connection) + + # Remove disconnected websockets + for websocket in disconnected_websockets: + self.disconnect(websocket) def get_historic(self): return self.historic_messages @@ -195,12 +213,20 @@ async def websocket_endpoint(websocket: WebSocket, client_id: int): "type": "control", "message": f"Client #{client_id} says: {data}", } - await manager.broadcast(json.dumps(message)) - # await manager.send_personal_message(f"You wrote: {data}", websocket) + try: + await manager.broadcast(json.dumps(message)) + except Exception as e: + logging.error(f"Error broadcasting message: {e}") except WebSocketDisconnect: manager.disconnect(websocket) - message = {"type": "control", "message": f"Client #{client_id} left the chat"} - await manager.broadcast(json.dumps(message)) + try: + message = {"type": "control", "message": f"Client #{client_id} left the chat"} + await manager.broadcast(json.dumps(message)) + except Exception as e: + logging.error(f"Error broadcasting disconnect message: {e}") + except Exception as e: + logging.error(f"WebSocket error: {e}") + manager.disconnect(websocket) templates = Jinja2Templates(directory=settings.templates_dir) @@ -917,7 +943,7 @@ async def nebula_monitor_log_error(scenario_name: str, id: str): async def nebula_monitor_image(scenario_name: str): topology_image = FileUtils.check_path(settings.config_dir, os.path.join(scenario_name, "topology.png")) if os.path.exists(topology_image): - return FileResponse(topology_image, media_type="image/png") + return FileResponse(topology_image, media_type="image/png", filename=f"{scenario_name}_topology.png") else: raise HTTPException(status_code=404, detail="Topology image not found") diff --git a/nebula/frontend/static/css/dashboard.css b/nebula/frontend/static/css/dashboard.css new file mode 100644 index 000000000..709874056 --- /dev/null +++ b/nebula/frontend/static/css/dashboard.css @@ -0,0 +1,262 @@ +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes float { + 0% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } + 100% { transform: translateY(0px); } +} + +.loading-pulse { + animation: pulse 2s infinite ease-in-out; +} + +.loading-float { + animation: float 3s infinite ease-in-out; +} + +.scenario-running-indicator { + position: fixed; + top: 6rem; + right: 2rem; + background: rgba(255, 193, 7, 0.1); + border: 2px solid #ffc107; + border-radius: 1rem; + padding: 1rem 1.5rem; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + z-index: 1000; + backdrop-filter: blur(8px); +} + +.scenario-running-indicator .spinner { + width: 1.5rem; + height: 1.5rem; + border: 3px solid rgba(255, 193, 7, 0.3); + border-top-color: #ffc107; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.progress-bar { + width: var(--progress-width); +} + +.bg-success-subtle { + background-color: rgba(25, 135, 84, 0.1); +} + +.bg-warning-subtle { + background-color: rgba(255, 193, 7, 0.1); +} + +.bg-danger-subtle { + background-color: rgba(220, 53, 69, 0.1); +} + +.bg-primary-subtle { + background-color: rgba(13, 110, 253, 0.1); +} + +/* Smooth transitions */ +.btn, .badge, .card { + transition: all 0.2s ease-in-out; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +#table-scenarios .btn i { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +/* Table styles */ +.table > :not(caption) > * > * { + padding: 1rem; +} + +.table tbody tr { + transition: background-color 0.2s ease; +} + +.table tbody tr:hover { + background-color: rgba(0, 0, 0, 0.02); +} + +/* Card styles */ +.card { + border-radius: 0.5rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1) !important; +} + +.card-header { + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +/* Progress bar */ +.progress { + height: 0.75rem; + border-radius: 1rem; + background-color: rgba(0, 0, 0, 0.05); +} + +.progress-bar { + border-radius: 1rem; + transition: width 0.6s ease; +} + +/* Tooltips */ +[title] { + position: relative; +} + +[title]:hover::after { + content: attr(title); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.8); + color: white; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; + white-space: nowrap; + z-index: 1000; + margin-bottom: 0.5rem; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; +} + +[title]:hover::after { + opacity: 1; + transform: translateX(-50%) translateY(-0.25rem); +} + +/* Icon styles */ +.fa { + font-size: 1rem; +} + +.fa-3x { + font-size: 3rem; +} + +/* Button styles */ +.btn-sm { + padding: 0.4rem 0.6rem; +} + +.btn-outline-primary:hover { + background-color: var(--bs-primary); + color: white; +} + +/* Badge styles */ +.badge { + font-weight: 500; +} + +/* Textarea styles */ +textarea.form-control { + border-radius: 0.375rem; + border: 1px solid rgba(0, 0, 0, 0.1); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +textarea.form-control:focus { + border-color: var(--bs-primary); + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Empty state styles */ +.empty-state-container { + max-width: 500px; + margin: 0 auto; +} + +.empty-state-container i { + display: inline-block; + margin-bottom: 1.5rem; + color: var(--bs-primary); + opacity: 0.9; +} + +.empty-state-container h3 { + font-size: 1.75rem; + margin-bottom: 1rem; +} + +.empty-state-container p { + font-size: 1.1rem; + line-height: 1.6; + color: #6c757d; +} + +/* Button styles */ +.btn-lg { + padding: 0.75rem 1.5rem; + font-size: 1.1rem; +} + +.btn-lg i { + font-size: 1.2rem; +} + +/* Card hover effect */ +.card { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1) !important; +} + +/* Progress bar animation */ +.progress-bar { + position: relative; + overflow: hidden; +} + +.progress-bar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.2) 50%, + rgba(255, 255, 255, 0) 100% + ); + animation: progress-shine 2s infinite; +} + +@keyframes progress-shine { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} \ No newline at end of file diff --git a/nebula/frontend/static/css/deployment.css b/nebula/frontend/static/css/deployment.css new file mode 100644 index 000000000..6231bc48b --- /dev/null +++ b/nebula/frontend/static/css/deployment.css @@ -0,0 +1,232 @@ + +.btn { + outline: none !important; + text-decoration: none !important; +} + +.row { + margin: 0; +} + +.participant-item { + padding: 5px; +} + +#form-configurations { + counter-reset: step; +} + +.step-title { + font-size: 15px; + font-weight: bold; + margin-top: 5px; +} + +.step-number::before { + content: counter(step); + counter-increment: step; + display: inline-block; + width: 30px; + height: 30px; + line-height: 30px; + border-radius: 50%; + background-color: #333; + color: #fff; + text-align: center; + margin-right: 10px; + font-size: 15px; +} + +/* Lock icons */ + +.icon-container { + cursor: pointer; +} + +input[type="checkbox"][id$="-lock"]:checked + .icon-container i::before { + content: "\f023"; +} + +input[type="checkbox"][id$="-lock"]:not(:checked) + .icon-container i::before { + content: "\f09c"; +} + +/* Tooltips */ +button[title] { + position: relative; +} + +button[title]:hover::after { + content: attr(title); + position: absolute; + top: -35px; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.8); + color: white; + padding: 8px 12px; + border-radius: 6px; + font-size: 14px; + white-space: nowrap; + z-index: 1000; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease, top 0.3s ease; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +button[title]:hover::after { + opacity: 1; + top: -50px; +} + +#expert-container .step-number::before { + background-color: #3333338a; + color: #fff; +} + +.participant-started { + background-color: rgb(255 165 0 / 51%) !important; + font-weight: bold !important; +} + +.participant-not-started { + background-color: #cecece !important; + font-weight: bold !important; +} + +.btn-info-participant { + background-color: #cecece !important; + font-weight: bold !important; +} + +#info-container { + padding: 10px; + margin-top: 5px; + position: absolute; + z-index: 1; + pointer-events: none; +} + +#legend-container { + border: 2px solid black; + background-color: #f2f2f2; + padding: 10px; + margin-top: 5px; + margin-left: 5px; + position: absolute; + z-index: 1; + pointer-events: none; +} + +#legend-label { + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + font-weight: bold; + margin-bottom: 2px; +} + +#legend { + display: flex; + align-items: center; + justify-content: center; +} + +.legend-item { + display: flex; + align-items: center; + margin-right: 5px; +} + +.legend-color { + display: block; + width: 10px; + height: 10px; + margin-right: 5px; +} + +.legend-color.circle { + border-radius: 50%; + background-color: #d95f02; +} + +.legend-color.triangle { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 10px solid #7570b3; + background-color: transparent; +} + +.legend-color.square { + background-color: #1b9e77; +} + +.legend-color.donut { + border: 3px solid #000000; + background-color: transparent; + border-radius: 50%; + box-sizing: border-box; +} + +.legend-text { + font-size: 12px; + font-weight: bold; +} + +.info-participants { + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + font-weight: bold; + margin-top: 5px; +} + +.popover { + max-width: 800px; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + /* Semi-transparent black */ + display: none; + z-index: 1000; +} + +#spinner { + display: none; + border: 7px solid #f3f3f3; + /* Light grey */ + border-top: 7px solid #3498db; + /* Blue */ + border-radius: 50%; + animation: spin 1s linear infinite; + /* Spin animation with 1 second duration */ + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80px; + height: 80px; + z-index: 1000; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/nebula/frontend/static/css/particles.css b/nebula/frontend/static/css/particles.css new file mode 100644 index 000000000..9e9959948 --- /dev/null +++ b/nebula/frontend/static/css/particles.css @@ -0,0 +1,18 @@ +canvas { + display: block; +} + +#particles-js { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: #ffffff; + background-image: url(""); + background-repeat: no-repeat; + background-size: cover; + background-position: 50% 50%; + z-index: -1; + pointer-events: auto; +} \ No newline at end of file diff --git a/nebula/frontend/static/css/style.css b/nebula/frontend/static/css/style.css index a374ef03f..1a136ed96 100755 --- a/nebula/frontend/static/css/style.css +++ b/nebula/frontend/static/css/style.css @@ -4,7 +4,7 @@ html, body { } body { - font-family: "Inter", sans-serif; + font-family: 'JetBrains Mono'; color: #444444; display: flex; flex-direction: column; @@ -24,15 +24,6 @@ a:hover { text-decoration: none; } -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: "Inter", sans-serif; -} - #preloader { position: fixed; top: 0; @@ -379,7 +370,7 @@ h6 { } section { - padding: 20px 0; + padding: 10px 0; overflow: hidden; } @@ -389,13 +380,18 @@ section { .alerts { z-index: 100; - position: fixed; + position: absolute; max-width: 100% !important; text-align: center; } .container { - max-width: 90%; + max-width: 80%; + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; } .colaboration { @@ -423,10 +419,10 @@ section { } } -.base .container { +/*.base .container { box-shadow: 0px 2px 15px rgba(0, 0, 0, 0.1); padding-bottom: 15px; -} +} */ .base .content { font-size: 15px; @@ -491,7 +487,6 @@ section { .about .count-box p { padding: 5px 0 0 0; margin: 0 0 0 60px; - font-family: "Inter", sans-serif; font-weight: 600; font-size: 14px; color: #2e4b5e; @@ -503,7 +498,6 @@ section { margin-top: 20px; color: #2e4b5e; font-size: 15px; - font-family: "Inter", sans-serif; transition: ease-in-out 0.3s; } @@ -575,16 +569,6 @@ div { text-align: justify; } -h1, -h2, -h3, -h4, -h5, -h6 { - text-align: left; - font-family: "Inter", sans-serif; -} - hr.styled { border: 0; height: 1px; @@ -597,7 +581,24 @@ hr.styled { } .table { - display: block !important; + width: 100% !important; + margin-bottom: 1rem; + color: #212529; +} + +.table td, .table th { + vertical-align: middle; + text-align: center; +} + +.table .d-flex { + justify-content: center; +} + +.table .badge { + display: inline-flex; + align-items: center; + justify-content: center; } .table-centered td, @@ -610,13 +611,15 @@ hr.styled { display: inline-block; } -.table-responsive>.table>thead>tr>th, -.table-responsive>.table>tbody>tr>th, -.table-responsive>.table>tfoot>tr>th, -.table-responsive>.table>thead>tr>td, -.table-responsive>.table>tbody>tr>td, -.table-responsive>.table>tfoot>tr>td { - white-space: nowrap; +.table-responsive { + width: 100%; + margin-bottom: 1rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.table-responsive>.table { + margin-bottom: 0; } @media screen and (max-width: 1530px) { @@ -627,6 +630,12 @@ hr.styled { -ms-overflow-style: -ms-autohiding-scrollbar; -webkit-overflow-scrolling: touch; } + + .container { + max-width: 100%; + padding-right: 10px; + padding-left: 10px; + } } td { @@ -645,12 +654,30 @@ td { max-width: 500px !important; } +.btn-danger { + color: #fff; + background-color: var(--bs-danger-bg-subtle)!important; + border-color: var(--bs-danger-border-subtle)!important; +} + +.btn-danger:hover { + color: #fff; + background-color: var(--bs-danger-border-subtle)!important; + border-color: var(--bs-danger-border-subtle)!important; +} + .btn-cream { color: #000000; background-color: #FFEFCB; border-color: #FFEFCB; } +.btn-cream:hover { + color: #000000; + background-color: #FFEFCB; + border-color: #FFEFCB; +} + .btn-light { color: #ffffff; background-color: #4d517d; @@ -675,6 +702,18 @@ td { border-color: #1d2253d2; } +.btn-dark-outline { + color: #1d2253; + background-color: transparent; + border-color: #1d2253; +} + +.btn-dark-outline:hover { + color: #ffffff; + background-color: #1d2253; + border-color: #1d2253; +} + .form-group { margin-bottom: 1rem !important; vertical-align: baseline !important; diff --git a/nebula/frontend/static/js/dashboard/config-manager.js b/nebula/frontend/static/js/dashboard/config-manager.js new file mode 100644 index 000000000..9913162cc --- /dev/null +++ b/nebula/frontend/static/js/dashboard/config-manager.js @@ -0,0 +1,39 @@ +// Configuration Manager Module +const ConfigManager = { + init() { + this.bindEvents(); + }, + + bindEvents() { + document.querySelectorAll('[id^=config-btn]').forEach(button => { + button.addEventListener('click', () => { + this.toggleConfigRow(button.dataset.scenarioName); + }); + }); + }, + + async toggleConfigRow(scenarioName) { + const configRow = document.getElementById(`config-row-${scenarioName}`); + const configTextElement = document.getElementById(`config-text-${scenarioName}`); + + if (configRow.style.display === 'none') { + try { + const response = await fetch(`/platform/dashboard/${scenarioName}/config`); + const data = await response.json(); + + if (data.status === 'success') { + configTextElement.value = JSON.stringify(data.config, null, 2); + } else { + configTextElement.value = 'No configuration available.'; + } + } catch (error) { + console.error('Error:', error); + alert('An error occurred while retrieving the configuration.'); + return; + } + } + configRow.style.display = configRow.style.display === 'none' ? '' : 'none'; + } +}; + +export default ConfigManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/dashboard/dashboard.js b/nebula/frontend/static/js/dashboard/dashboard.js new file mode 100644 index 000000000..4c2bdcc74 --- /dev/null +++ b/nebula/frontend/static/js/dashboard/dashboard.js @@ -0,0 +1,31 @@ +import ScenarioActions from './scenario-actions.js'; +import NotesManager from './notes-manager.js'; +import ConfigManager from './config-manager.js'; + +// Main Dashboard Module +const Dashboard = { + init() { + this.initializeModules(); + // Only show demo message if user is not logged in + if (typeof window.userLoggedIn === 'boolean' && !window.userLoggedIn) { + this.checkDemoMode(); + } + }, + + initializeModules() { + ScenarioActions.init(); + NotesManager.init(); + ConfigManager.init(); + }, + + checkDemoMode() { + showAlert('info', 'Some functionalities are disabled in the demo version. Please, log in to access all functionalities.'); + } +}; + +// Initialize dashboard when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + Dashboard.init(); +}); + +export default Dashboard; \ No newline at end of file diff --git a/nebula/frontend/static/js/dashboard/notes-manager.js b/nebula/frontend/static/js/dashboard/notes-manager.js new file mode 100644 index 000000000..8896f5be3 --- /dev/null +++ b/nebula/frontend/static/js/dashboard/notes-manager.js @@ -0,0 +1,75 @@ +// Notes Manager Module +const NotesManager = { + init() { + this.bindEvents(); + }, + + bindEvents() { + document.querySelectorAll('[id^=note-btn]').forEach(button => { + button.addEventListener('click', () => { + this.toggleNotesRow(button.dataset.scenarioName); + }); + }); + + document.querySelectorAll('[id^=save-note]').forEach(button => { + button.addEventListener('click', () => { + this.saveNotes(button.dataset.scenarioName); + }); + }); + }, + + async toggleNotesRow(scenarioName) { + const notesRow = document.getElementById(`notes-row-${scenarioName}`); + const notesTextElement = document.getElementById(`notes-text-${scenarioName}`); + + if (notesRow.style.display === 'none') { + try { + const response = await fetch(`/platform/dashboard/${scenarioName}/notes`); + const data = await response.json(); + + if (data.status === 'success') { + notesTextElement.value = data.notes; + } else { + notesTextElement.value = ''; + } + } catch (error) { + console.error('Error:', error); + alert('An error occurred while retrieving the notes.'); + return; + } + } + + notesRow.style.display = notesRow.style.display === 'none' ? '' : 'none'; + }, + + async saveNotes(scenarioName) { + const notesText = document.getElementById(`notes-text-${scenarioName}`).value; + + try { + const response = await fetch(`/platform/dashboard/${scenarioName}/save_note`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ notes: notesText }), + }); + + const data = await response.json(); + + if (data.status === 'success') { + showAlert('success', 'Notes saved successfully'); + } else { + if (data.code === 401) { + showAlert('info', 'Some functionalities are disabled in the demo version. Please, log in to access all functionalities.'); + } else { + showAlert('error', 'Failed to save notes'); + } + } + } catch (error) { + console.error('Error:', error); + showAlert('error', 'Failed to save notes'); + } + } +}; + +export default NotesManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/dashboard/scenario-actions.js b/nebula/frontend/static/js/dashboard/scenario-actions.js new file mode 100644 index 000000000..d5cd915de --- /dev/null +++ b/nebula/frontend/static/js/dashboard/scenario-actions.js @@ -0,0 +1,81 @@ +// Scenario Actions Module +const ScenarioActions = { + init() { + this.bindEvents(); + }, + + bindEvents() { + $(document).on('click', '#relaunch-btn', this.handleRelaunch.bind(this)); + $(document).on('click', '#remove-btn', this.handleRemove.bind(this)); + }, + + handleRelaunch(event) { + const scenarioName = $(event.currentTarget).data('scenario-name'); + const scenarioTitle = $(event.currentTarget).data('scenario-title'); + + $('#confirm-modal').modal('show'); + $('#confirm-modal .modal-title').text('Relaunch scenario'); + $('#confirm-modal #confirm-modal-body').html(`Are you sure you want to relaunch the scenario ${scenarioTitle}?`); + + $('#confirm-modal #yes-button').off('click').on('click', () => { + this.executeRelaunch(scenarioName); + }); + }, + + handleRemove(event) { + const scenarioName = $(event.currentTarget).data('scenario-name'); + + $('#confirm-modal').modal('show'); + $('#confirm-modal .modal-title').text('Remove scenario'); + $('#confirm-modal #confirm-modal-body').html( + `Are you sure you want to remove the scenario ${scenarioName}?

` + + `

Warning: you will remove the scenario from the database

` + ); + + $('#confirm-modal #yes-button').off('click').on('click', () => { + this.executeRemove(scenarioName); + }); + }, + + async executeRelaunch(scenarioName) { + try { + const response = await fetch(`/platform/dashboard/${scenarioName}/relaunch`, { + method: 'GET' + }); + + if (response.redirected) { + window.location.href = response.url; + } else { + $('#confirm-modal').modal('hide'); + $('#confirm-modal').on('hidden.bs.modal', () => { + $('#info-modal-body').html('You are not allowed to relaunch a scenario with demo role.'); + $('#info-modal').modal('show'); + }); + } + } catch (error) { + console.error('Error:', error); + } + }, + + async executeRemove(scenarioName) { + try { + const response = await fetch(`/platform/dashboard/${scenarioName}/remove`, { + method: 'GET' + }); + + if (response.redirected) { + window.location.href = response.url; + } else { + $('#confirm-modal').modal('hide'); + $('#confirm-modal').on('hidden.bs.modal', () => { + $('#info-modal-body').html('You are not allowed to remove a scenario with demo role.'); + $('#info-modal').modal('show'); + }); + } + } catch (error) { + console.error('Error:', error); + } + } +}; + +export default ScenarioActions; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/attack.js b/nebula/frontend/static/js/deployment/attack.js new file mode 100644 index 000000000..4bc208eb4 --- /dev/null +++ b/nebula/frontend/static/js/deployment/attack.js @@ -0,0 +1,226 @@ +// Attack Configuration Module +const AttackManager = (function() { + const ATTACK_TYPES = { + NO_ATTACK: 'No Attack', + LABEL_FLIPPING: 'Label Flipping', + SAMPLE_POISONING: 'Sample Poisoning', + MODEL_POISONING: 'Model Poisoning', + GLL_NEURON_INVERSION: 'GLL Neuron Inversion', + SWAPPING_WEIGHTS: 'Swapping Weights', + DELAYER: 'Delayer', + FLOODING: 'Flooding' + }; + + function updateAttackUI(attackType) { + const elements = { + poisonedNode: {title: document.getElementById("poisoned-node-title"), container: document.getElementById("poisoned-node-percent-container")}, + poisonedSample: {title: document.getElementById("poisoned-sample-title"), container: document.getElementById("poisoned-sample-percent-container")}, + poisonedNoise: {title: document.getElementById("poisoned-noise-title"), container: document.getElementById("poisoned-noise-percent-container")}, + noiseType: {title: document.getElementById("noise-type-title"), container: document.getElementById("noise-type-container")}, + targeted: {title: document.getElementById("targeted-title"), container: document.getElementById("targeted-container")}, + targetLabel: {title: document.getElementById("target_label-title"), container: document.getElementById("target_label-container")}, + targetChangedLabel: {title: document.getElementById("target_changed_label-title"), container: document.getElementById("target_changed_label-container")}, + layerIdx: {title: document.getElementById("layer_idx-title"), container: document.getElementById("layer_idx-container")}, + delay: {title: document.getElementById("delay-title"), container: document.getElementById("delay-container")}, + startAttack: {title: document.getElementById("start-attack-title"), container: document.getElementById("start-attack-container")}, + stopAttack: {title: document.getElementById("stop-attack-title"), container: document.getElementById("stop-attack-container")}, + attackInterval: {title: document.getElementById("attack-interval-title"), container: document.getElementById("attack-interval-container")}, + targetPercentage: {title: document.getElementById("target-percentage-title"), container: document.getElementById("target-percentage-container")}, + selectionInterval: {title: document.getElementById("selection-interval-title"), container: document.getElementById("selection-interval-container")}, + floodingFactor: {title: document.getElementById("flooding-factor-title"), container: document.getElementById("flooding-factor-container")} + }; + + // Hide all elements first + Object.values(elements).forEach(element => { + element.title.style.display = "none"; + element.container.style.display = "none"; + }); + + // Show relevant elements based on attack type + switch(attackType) { + case ATTACK_TYPES.NO_ATTACK: + break; + + case ATTACK_TYPES.LABEL_FLIPPING: + showElements(elements, ['poisonedNode', 'poisonedSample', 'targeted', 'startAttack', 'stopAttack', 'attackInterval']); + if(document.getElementById("targeted").checked) { + showElements(elements, ['targetLabel', 'targetChangedLabel']); + } + break; + + case ATTACK_TYPES.SAMPLE_POISONING: + showElements(elements, ['poisonedNode', 'poisonedSample', 'poisonedNoise', 'noiseType', 'targeted', 'startAttack', 'stopAttack', 'attackInterval']); + break; + + case ATTACK_TYPES.MODEL_POISONING: + showElements(elements, ['poisonedNode', 'poisonedNoise', 'noiseType', 'startAttack', 'stopAttack', 'attackInterval']); + break; + + case ATTACK_TYPES.GLL_NEURON_INVERSION: + showElements(elements, ['poisonedNode', 'startAttack', 'stopAttack', 'attackInterval']); + break; + + case ATTACK_TYPES.SWAPPING_WEIGHTS: + showElements(elements, ['poisonedNode', 'layerIdx', 'startAttack', 'stopAttack', 'attackInterval']); + break; + + case ATTACK_TYPES.DELAYER: + showElements(elements, ['poisonedNode', 'delay', 'startAttack', 'stopAttack', 'attackInterval', 'targetPercentage', 'selectionInterval']); + break; + + case ATTACK_TYPES.FLOODING: + showElements(elements, ['poisonedNode', 'startAttack', 'stopAttack', 'attackInterval', 'targetPercentage', 'selectionInterval', 'floodingFactor']); + break; + } + } + + function showElements(elements, elementKeys) { + elementKeys.forEach(key => { + elements[key].title.style.display = "block"; + elements[key].container.style.display = "block"; + }); + } + + function initializeEventListeners() { + document.getElementById("poisoning-attack-select").addEventListener("change", function() { + updateAttackUI(this.value); + }); + + document.getElementById("targeted").addEventListener("change", function() { + const attackType = document.getElementById("poisoning-attack-select").value; + updateAttackUI(attackType); + }); + + document.getElementById("malicious-nodes-select").addEventListener("change", function() { + const poisonedNodePercent = document.getElementById("poisoned-node-percent"); + if(this.value === "Manual") { + poisonedNodePercent.value = 0; + poisonedNodePercent.disabled = true; + } else { + poisonedNodePercent.disabled = false; + } + }); + } + + function getAttackConfig() { + const attackType = document.getElementById("poisoning-attack-select").value; + const config = { + type: attackType, + poisonedNodePercent: parseFloat(document.getElementById("poisoned-node-percent").value), + startRound: parseInt(document.getElementById("start-attack").value), + stopRound: parseInt(document.getElementById("stop-attack").value), + attackInterval: parseInt(document.getElementById("attack-interval").value) + }; + + switch(attackType) { + case ATTACK_TYPES.LABEL_FLIPPING: + config.poisonedSamplePercent = parseFloat(document.getElementById("poisoned-sample-percent").value); + config.targeted = document.getElementById("targeted").checked; + if(config.targeted) { + config.targetLabel = parseInt(document.getElementById("target_label").value); + config.targetChangedLabel = parseInt(document.getElementById("target_changed_label").value); + } + break; + + case ATTACK_TYPES.SAMPLE_POISONING: + config.poisonedSamplePercent = parseFloat(document.getElementById("poisoned-sample-percent").value); + config.poisonedNoisePercent = parseFloat(document.getElementById("poisoned-noise-percent").value); + config.noiseType = document.getElementById("noise_type").value; + config.targeted = document.getElementById("targeted").checked; + break; + + case ATTACK_TYPES.MODEL_POISONING: + config.poisonedNoisePercent = parseFloat(document.getElementById("poisoned-noise-percent").value); + config.noiseType = document.getElementById("noise_type").value; + break; + + case ATTACK_TYPES.SWAPPING_WEIGHTS: + config.layerIdx = parseInt(document.getElementById("layer_idx").value); + break; + + case ATTACK_TYPES.DELAYER: + config.delay = parseInt(document.getElementById("delay").value); + config.targetPercentage = parseFloat(document.getElementById("target-percentage").value); + config.selectionInterval = parseInt(document.getElementById("selection-interval").value); + break; + + case ATTACK_TYPES.FLOODING: + config.floodingFactor = parseInt(document.getElementById("flooding-factor").value); + config.targetPercentage = parseFloat(document.getElementById("target-percentage").value); + config.selectionInterval = parseInt(document.getElementById("selection-interval").value); + break; + } + + return config; + } + + function setAttackConfig(config) { + if (!config) return; + + // Set attack type and update UI + document.getElementById("poisoning-attack-select").value = config.type; + updateAttackUI(config.type); + + // Set common fields + document.getElementById("poisoned-node-percent").value = config.poisonedNodePercent || 0; + document.getElementById("start-attack").value = config.startRound || 1; + document.getElementById("stop-attack").value = config.stopRound || 10; + document.getElementById("attack-interval").value = config.attackInterval || 1; + + // Set attack-specific fields + switch(config.type) { + case ATTACK_TYPES.LABEL_FLIPPING: + document.getElementById("poisoned-sample-percent").value = config.poisonedSamplePercent || 0; + document.getElementById("targeted").checked = config.targeted || false; + if(config.targeted) { + document.getElementById("target_label").value = config.targetLabel || 4; + document.getElementById("target_changed_label").value = config.targetChangedLabel || 7; + } + break; + + case ATTACK_TYPES.SAMPLE_POISONING: + document.getElementById("poisoned-sample-percent").value = config.poisonedSamplePercent || 0; + document.getElementById("poisoned-noise-percent").value = config.poisonedNoisePercent || 0; + document.getElementById("noise_type").value = config.noiseType || "Salt"; + document.getElementById("targeted").checked = config.targeted || false; + break; + + case ATTACK_TYPES.MODEL_POISONING: + document.getElementById("poisoned-noise-percent").value = config.poisonedNoisePercent || 0; + document.getElementById("noise_type").value = config.noiseType || "Salt"; + break; + + case ATTACK_TYPES.SWAPPING_WEIGHTS: + document.getElementById("layer_idx").value = config.layerIdx || 0; + break; + + case ATTACK_TYPES.DELAYER: + document.getElementById("delay").value = config.delay || 10; + document.getElementById("target-percentage").value = config.targetPercentage || 100; + document.getElementById("selection-interval").value = config.selectionInterval || 1; + break; + + case ATTACK_TYPES.FLOODING: + document.getElementById("flooding-factor").value = config.floodingFactor || 100; + document.getElementById("target-percentage").value = config.targetPercentage || 100; + document.getElementById("selection-interval").value = config.selectionInterval || 1; + break; + } + } + + function resetAttackConfig() { + document.getElementById("poisoning-attack-select").value = ATTACK_TYPES.NO_ATTACK; + updateAttackUI(ATTACK_TYPES.NO_ATTACK); + } + + return { + ATTACK_TYPES, + initializeEventListeners, + updateAttackUI, + getAttackConfig, + setAttackConfig, + resetAttackConfig + }; +})(); + +export default AttackManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/graph-settings.js b/nebula/frontend/static/js/deployment/graph-settings.js new file mode 100644 index 000000000..0c3d93be6 --- /dev/null +++ b/nebula/frontend/static/js/deployment/graph-settings.js @@ -0,0 +1,41 @@ +// Graph Settings Module +const GraphSettings = (function() { + const Settings = { + solidDistance: 50, + Distance: 50 + }; + + function initializeDistanceControls() { + const distanceInput = document.getElementById('distanceInput'); + const distanceValue = document.getElementById('distanceValue'); + + distanceInput.addEventListener('input', function() { + distanceValue.value = distanceInput.value; + Settings.Distance = distanceInput.value; + updateLinkDistance(); + }); + + distanceValue.addEventListener('input', function() { + distanceInput.value = distanceValue.value; + Settings.Distance = distanceValue.value; + updateLinkDistance(); + }); + } + + function updateLinkDistance() { + const Graph = window.TopologyManager.getGraph(); + if (Graph) { + Graph.d3Force('link') + .distance(link => link.color ? Settings.solidDistance : Settings.Distance); + Graph.numDimensions(3); // Re-heat simulation + } + } + + return { + initializeDistanceControls, + updateLinkDistance, + getSettings: () => Settings + }; +})(); + +export default GraphSettings; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/help-content.js b/nebula/frontend/static/js/deployment/help-content.js new file mode 100644 index 000000000..afd53c50e --- /dev/null +++ b/nebula/frontend/static/js/deployment/help-content.js @@ -0,0 +1,177 @@ +// Help Content Module +const HelpContent = (function() { + function initializePopovers() { + const tooltipElements = { + 'processHelpIcon': 'Process deployment allows you to deploy participants in the same machine using different processes.', + 'dockerHelpIcon': 'Docker deployment allows you to deploy participants in different containers.', + 'architectureHelpIcon': architecture, + 'topologyCustomIcon': topology.custom, + 'topologyPredefinedIcon': topology.predefined, + 'datasetHelpIcon': dataset, + 'iidHelpIcon': iid, + 'partitionMethodsHelpIcon': partitionMethods, + 'parameterSettingHelpIcon': parameterSetting, + 'modelHelpIcon': model, + 'maliciousHelpIcon': malicious + }; + + Object.entries(tooltipElements).forEach(([id, content]) => { + const element = document.getElementById(id); + if (element) { + new bootstrap.Tooltip(element, { + title: content, + html: true, + placement: 'right' + }); + } + }); + } + + const topology = { + custom: `
+ Custom Topology + +
`, + predefined: `
+ Predefined Topologies + +
` + }; + + const architecture = `
+ Federation Architectures + +
`; + + const dataset = `
+ Available Datasets + +
`; + + const iid = `
+ Data Distribution Types + +
`; + + const partitionMethods = `
+ Partition Methods + +
`; + + const parameterSetting = `
+ Parameter Settings + +
`; + + const model = `
+ Available Models + +
`; + + const malicious = `
+ Malicious Node Selection + +
`; + + const deployment = { + process: `
+ Process Deployment + +
`, + docker: `
+ Docker Deployment + +
` + }; + + const reputation = { + initialization: `
+ Reputation Initialization +

Initial reputation value for all participants

+
`, + weighting: `
+ Weighting Factor +

Use dynamic or static weighting factor for reputation

+
` + }; + + return { + initializePopovers, + topology, + architecture, + dataset, + iid, + partitionMethods, + parameterSetting, + model, + malicious, + deployment, + reputation + }; +})(); + +export default HelpContent; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/main.js b/nebula/frontend/static/js/deployment/main.js new file mode 100644 index 000000000..37607713a --- /dev/null +++ b/nebula/frontend/static/js/deployment/main.js @@ -0,0 +1,257 @@ +// Main Deployment Module +import ScenarioManager from './scenario.js'; +import TopologyManager from './topology.js'; +import AttackManager from './attack.js'; +import MobilityManager from './mobility.js'; +import ReputationManager from './reputation.js'; +import GraphSettings from './graph-settings.js'; +import Utils from './utils.js'; + +const DeploymentManager = (function() { + function initialize() { + // First initialize all modules + initializeModules(); + + // Then initialize event listeners and UI controls + initializeEventListeners(); + setupDeploymentButtons(); + initializeSelectElements(); + + // Finally initialize scenarios after all modules are ready + ScenarioManager.initializeScenarios(); + } + + function initializeModules() { + // Initialize all sub-modules + TopologyManager.initializeGraph('3d-graph', getGraphWidth(), getGraphHeight()); + AttackManager.initializeEventListeners(); + MobilityManager.initializeMobility(); + ReputationManager.initializeReputationSystem(); + GraphSettings.initializeDistanceControls(); + + // Make modules globally available + window.ScenarioManager = ScenarioManager; + window.TopologyManager = TopologyManager; + window.AttackManager = AttackManager; + window.MobilityManager = MobilityManager; + window.ReputationManager = ReputationManager; + window.GraphSettings = GraphSettings; + } + + function getGraphWidth() { + return document.getElementById('3d-graph').offsetWidth; + } + + function getGraphHeight() { + return document.getElementById('3d-graph').offsetHeight; + } + + function initializeEventListeners() { + window.addEventListener("resize", handleResize); + window.addEventListener("click", handleOutsideClick); + setupDatasetListeners(); + setupInputValidation(); + } + + function handleResize() { + TopologyManager.updateGraph(); + } + + function handleOutsideClick(event) { + if (!event.target.matches('.dropdown-item')) { + const dropdowns = document.getElementsByClassName("dropdown-menu"); + Array.from(dropdowns).forEach(dropdown => { + if (dropdown.style.display === "block") { + dropdown.style.display = "none"; + } + }); + } + } + + function setupDeploymentButtons() { + const runBtn = document.getElementById("run-btn"); + if (runBtn) { + runBtn.addEventListener("click", () => { + if (!validateScenario()) { + return; + } + window.UIControls.handleDeployment(); + }); + } + } + + function validateScenario() { + // Validate topology + if (!TopologyManager.getData().nodes.length) { + Utils.showAlert('error', 'Please create a topology with at least one node'); + return false; + } + + // Validate start node + if (!document.querySelector(".participant-started")) { + Utils.showAlert('error', 'Please select one "start" participant for the scenario'); + return false; + } + + return true; + } + + function setupDatasetListeners() { + const datasetSelect = document.getElementById("datasetSelect"); + if (datasetSelect) { + datasetSelect.addEventListener("change", () => { + // Update model options based on selected dataset + updateModelOptions(datasetSelect.value); + }); + } + } + + function initializeSelectElements() { + // Initialize dataset options + populateDatasetOptions(); + + // Initialize topology options + const topologySelect = document.getElementById("predefined-topology-select"); + if (topologySelect) { + const topologies = ['Fully', 'Ring', 'Star', 'Random']; + topologies.forEach(topology => { + if (!topologySelect.querySelector(`option[value="${topology}"]`)) { + const option = document.createElement("option"); + option.value = topology; + option.textContent = topology; + topologySelect.appendChild(option); + } + }); + topologySelect.value = 'Fully'; + } + + // Initialize IID options + const iidSelect = document.getElementById("iidSelect"); + if (iidSelect) { + const iidOptions = [ + { value: 'true', text: 'IID' }, + { value: 'false', text: 'Non-IID' } + ]; + iidOptions.forEach(opt => { + if (!iidSelect.querySelector(`option[value="${opt.value}"]`)) { + const option = document.createElement("option"); + option.value = opt.value; + option.textContent = opt.text; + iidSelect.appendChild(option); + } + }); + iidSelect.value = 'false'; + } + + // Initialize partition options + const partitionSelect = document.getElementById("partitionSelect"); + if (partitionSelect) { + const partitionOptions = [ + { value: 'dirichlet', text: 'Dirichlet' }, + { value: 'percent', text: 'Percentage' }, + { value: 'balancediid', text: 'Balanced IID' }, + { value: 'unbalancediid', text: 'Unbalanced IID' } + ]; + partitionOptions.forEach(opt => { + if (!partitionSelect.querySelector(`option[value="${opt.value}"]`)) { + const option = document.createElement("option"); + option.value = opt.value; + option.textContent = opt.text; + option.disabled = opt.value === 'balancediid' || opt.value === 'unbalancediid'; + option.style.display = opt.value === 'balancediid' || opt.value === 'unbalancediid' ? 'none' : 'block'; + partitionSelect.appendChild(option); + } + }); + partitionSelect.value = 'dirichlet'; + } + + // Initialize logging level options + const loggingLevel = document.getElementById("loggingLevel"); + if (loggingLevel) { + const loggingOptions = [ + { value: 'false', text: 'Only alerts' }, + { value: 'true', text: 'Alerts and logs' } + ]; + loggingOptions.forEach(opt => { + if (!loggingLevel.querySelector(`option[value="${opt.value}"]`)) { + const option = document.createElement("option"); + option.value = opt.value; + option.textContent = opt.text; + loggingLevel.appendChild(option); + } + }); + loggingLevel.value = 'true'; + } + } + + function populateDatasetOptions() { + const datasetSelect = document.getElementById("datasetSelect"); + if (!datasetSelect) return; + + // Clear existing options + datasetSelect.innerHTML = ""; + + // Add dataset options + const datasets = ['MNIST', 'FashionMNIST', 'CIFAR10']; + datasets.forEach(dataset => { + const option = document.createElement("option"); + option.value = dataset; + option.textContent = dataset; + datasetSelect.appendChild(option); + }); + + // Set default value and trigger change event + datasetSelect.value = 'MNIST'; + datasetSelect.dispatchEvent(new Event('change')); + } + + function updateModelOptions(dataset) { + const modelSelect = document.getElementById("modelSelect"); + if (!modelSelect) return; + + // Clear existing options + modelSelect.innerHTML = ""; + + // Add appropriate models based on dataset + const models = getModelsForDataset(dataset); + models.forEach(model => { + const option = document.createElement("option"); + option.value = model; + option.textContent = model; + modelSelect.appendChild(option); + }); + } + + function getModelsForDataset(dataset) { + // Return appropriate models based on dataset + switch(dataset.toLowerCase()) { + case 'mnist': + case 'fashionmnist': + return ['MLP', 'CNN']; + case 'cifar10': + return ['CNN', 'ResNet18']; + default: + return ['MLP', 'CNN']; + } + } + + function setupInputValidation() { + const numericInputs = document.querySelectorAll('input[type="number"]'); + numericInputs.forEach(input => { + if(input.hasAttribute('min')) { + input.addEventListener('input', () => Utils.greaterThan0(input)); + } + if(input.hasAttribute('min') && input.hasAttribute('max')) { + input.addEventListener('input', () => Utils.isInRange(input, + parseInt(input.getAttribute('min')), + parseInt(input.getAttribute('max')))); + } + }); + } + + return { + initialize + }; +})(); + +export default DeploymentManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/mobility.js b/nebula/frontend/static/js/deployment/mobility.js new file mode 100644 index 000000000..4785a2ec5 --- /dev/null +++ b/nebula/frontend/static/js/deployment/mobility.js @@ -0,0 +1,288 @@ +// Mobility Configuration Module +const MobilityManager = { + map: null, + + initializeMobility() { + this.setupLocationControls(); + this.setupMobilityControls(); + this.setupAdditionalParticipants(); + }, + + setupLocationControls() { + const customLocationDiv = document.getElementById("mobility-custom-location"); + + document.getElementById("random-geo-btn").addEventListener("click", () => { + customLocationDiv.style.display = "none"; + }); + + document.getElementById("custom-location-btn").addEventListener("click", () => { + customLocationDiv.style.display = "block"; + }); + + document.getElementById("current-location-btn").addEventListener("click", () => { + navigator.geolocation.getCurrentPosition(position => { + document.getElementById("latitude").value = position.coords.latitude; + document.getElementById("longitude").value = position.coords.longitude; + if (this.map) { + this.updateMapMarker(position.coords.latitude, position.coords.longitude); + } + }); + }); + + document.getElementById("open-map-btn").addEventListener("click", () => { + const mapContainer = document.getElementById("map-container"); + if (mapContainer.style.display === "none") { + mapContainer.style.display = "block"; + this.initializeMap(); + } else { + mapContainer.style.display = "none"; + } + }); + }, + + setupMobilityControls() { + const mobilityOptionsDiv = document.getElementById("mobility-options"); + + document.getElementById("without-mobility-btn").addEventListener("click", () => { + mobilityOptionsDiv.style.display = "none"; + if (this.map) { + this.removeMapCircle(); + } + }); + + document.getElementById("mobility-btn").addEventListener("click", () => { + mobilityOptionsDiv.style.display = "block"; + if (this.map) { + this.addMapCircle(); + } + }); + + document.getElementById("radiusFederation").addEventListener("change", () => { + if (this.map && document.getElementById("mobility-btn").checked) { + this.updateMapCircle(); + } + }); + }, + + initializeMap() { + if (!this.map) { + this.map = L.map('map').setView([38.023522, -1.174389], 17); + + L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '© enriquetomasmb.com', + maxZoom: 18, + }).addTo(this.map); + + this.addInitialMarker(); + if (document.getElementById("mobility-btn").checked) { + this.addMapCircle(); + } + + this.map.on('click', this.handleMapClick.bind(this)); + } + }, + + addInitialMarker() { + const lat = parseFloat(document.getElementById("latitude").value); + const lng = parseFloat(document.getElementById("longitude").value); + this.updateMapMarker(lat, lng); + }, + + handleMapClick(e) { + this.updateMapMarker(e.latlng.lat, e.latlng.lng); + document.getElementById("latitude").value = e.latlng.lat; + document.getElementById("longitude").value = e.latlng.lng; + }, + + updateMapMarker(lat, lng) { + this.map.eachLayer(layer => { + if (layer instanceof L.Marker) { + this.map.removeLayer(layer); + } + }); + L.marker([lat, lng]).addTo(this.map); + this.updateMapCircle(); + }, + + addMapCircle() { + const lat = parseFloat(document.getElementById("latitude").value); + const lng = parseFloat(document.getElementById("longitude").value); + const radius = parseInt(document.getElementById("radiusFederation").value); + + L.circle([lat, lng], { + color: 'red', + fillColor: '#f03', + fillOpacity: 0.4, + radius: radius + }).addTo(this.map); + }, + + updateMapCircle() { + this.removeMapCircle(); + if (document.getElementById("mobility-btn").checked) { + this.addMapCircle(); + } + }, + + removeMapCircle() { + this.map.eachLayer(layer => { + if (layer instanceof L.Circle) { + this.map.removeLayer(layer); + } + }); + }, + + setupAdditionalParticipants() { + document.getElementById("additionalParticipants").addEventListener("change", function() { + const container = document.getElementById("additional-participants-items"); + container.innerHTML = ""; + + for (let i = 0; i < this.value; i++) { + const participantItem = this.createParticipantItem(i); + container.appendChild(participantItem); + } + }.bind(this)); + }, + + createParticipantItem(index) { + const participantItem = document.createElement("div"); + participantItem.style.marginLeft = "20px"; + participantItem.classList.add("additional-participant-item"); + + const heading = document.createElement("h5"); + heading.textContent = `Round of deployment (participant ${index + 1})`; + + const input = document.createElement("input"); + input.type = "number"; + input.classList.add("form-control"); + input.id = `roundsAdditionalParticipant${index}`; + input.placeholder = "round"; + input.min = "1"; + input.value = "1"; + input.style.display = "inline"; + input.style.width = "20%"; + + participantItem.appendChild(heading); + participantItem.appendChild(input); + + return participantItem; + }, + + getMobilityConfig() { + const config = { + enabled: document.getElementById("mobility-btn").checked, + randomGeo: document.getElementById("random-geo-btn").checked, + location: { + latitude: parseFloat(document.getElementById("latitude").value), + longitude: parseFloat(document.getElementById("longitude").value) + }, + mobilityType: document.getElementById("mobilitySelect").value, + radiusFederation: parseInt(document.getElementById("radiusFederation").value), + schemeMobility: document.getElementById("schemeMobilitySelect").value, + roundFrequency: parseInt(document.getElementById("roundFrequency").value), + mobileParticipantsPercent: parseInt(document.getElementById("mobileParticipantsPercent").value), + additionalParticipants: [] + }; + + const additionalParticipantsCount = parseInt(document.getElementById("additionalParticipants").value); + for (let i = 0; i < additionalParticipantsCount; i++) { + config.additionalParticipants.push({ + round: parseInt(document.getElementById(`roundsAdditionalParticipant${i}`).value) + }); + } + + return config; + }, + + setMobilityConfig(config) { + if (!config) return; + + // Validate required properties + if (typeof config.enabled !== 'boolean') { + console.warn('Invalid mobility config: enabled must be a boolean'); + return; + } + + if (typeof config.randomGeo !== 'boolean') { + console.warn('Invalid mobility config: randomGeo must be a boolean'); + return; + } + + if (config.location && (typeof config.location.latitude !== 'number' || typeof config.location.longitude !== 'number')) { + console.warn('Invalid mobility config: location must have numeric latitude and longitude'); + return; + } + + // Set mobility enabled/disabled + document.getElementById("mobility-btn").checked = config.enabled; + document.getElementById("without-mobility-btn").checked = !config.enabled; + document.getElementById("mobility-options").style.display = config.enabled ? "block" : "none"; + + // Set location type and coordinates + document.getElementById("random-geo-btn").checked = config.randomGeo; + document.getElementById("custom-location-btn").checked = !config.randomGeo; + document.getElementById("mobility-custom-location").style.display = config.randomGeo ? "none" : "block"; + + if (config.location) { + document.getElementById("latitude").value = config.location.latitude; + document.getElementById("longitude").value = config.location.longitude; + if (this.map) { + this.updateMapMarker(config.location.latitude, config.location.longitude); + } + } + + // Set mobility settings + document.getElementById("mobilitySelect").value = config.mobilityType || "both"; + document.getElementById("radiusFederation").value = config.radiusFederation || 100; + document.getElementById("schemeMobilitySelect").value = config.schemeMobility || "random"; + document.getElementById("roundFrequency").value = config.roundFrequency || 1; + document.getElementById("mobileParticipantsPercent").value = config.mobileParticipantsPercent || 100; + + // Set additional participants + if (config.additionalParticipants) { + if (!Array.isArray(config.additionalParticipants)) { + console.warn('Invalid mobility config: additionalParticipants must be an array'); + return; + } + + document.getElementById("additionalParticipants").value = config.additionalParticipants.length; + const container = document.getElementById("additional-participants-items"); + container.innerHTML = ""; + + config.additionalParticipants.forEach((participant, index) => { + if (typeof participant.round !== 'number') { + console.warn(`Invalid mobility config: participant ${index} round must be a number`); + return; + } + const participantItem = this.createParticipantItem(index); + document.getElementById(`roundsAdditionalParticipant${index}`).value = participant.round; + container.appendChild(participantItem); + }); + } + }, + + resetMobilityConfig() { + // Reset to default values + document.getElementById("without-mobility-btn").checked = true; + document.getElementById("mobility-options").style.display = "none"; + document.getElementById("random-geo-btn").checked = false; + document.getElementById("custom-location-btn").checked = true; + document.getElementById("mobility-custom-location").style.display = "block"; + document.getElementById("latitude").value = "38.023522"; + document.getElementById("longitude").value = "-1.174389"; + document.getElementById("mobilitySelect").value = "both"; + document.getElementById("radiusFederation").value = "100"; + document.getElementById("schemeMobilitySelect").value = "random"; + document.getElementById("roundFrequency").value = "1"; + document.getElementById("mobileParticipantsPercent").value = "100"; + document.getElementById("additionalParticipants").value = "0"; + document.getElementById("additional-participants-items").innerHTML = ""; + + if (this.map) { + this.updateMapMarker(38.023522, -1.174389); + this.removeMapCircle(); + } + } +}; + +export default MobilityManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/reputation.js b/nebula/frontend/static/js/deployment/reputation.js new file mode 100644 index 000000000..f65e1595f --- /dev/null +++ b/nebula/frontend/static/js/deployment/reputation.js @@ -0,0 +1,164 @@ +// Reputation System Module +const ReputationManager = (function() { + function initializeReputationSystem() { + setupReputationSwitch(); + setupWeightingFactor(); + setupWeightValidation(); + setupInitialReputation(); + } + + function setupReputationSwitch() { + document.getElementById("reputationSwitch").addEventListener("change", function() { + const reputationMetrics = document.getElementById("reputation-metrics"); + const reputationSettings = document.getElementById("reputation-settings"); + const weightingSettings = document.getElementById("weighting-settings"); + + reputationMetrics.style.display = this.checked ? "block" : "none"; + reputationSettings.style.display = this.checked ? "block" : "none"; + weightingSettings.style.display = this.checked ? "block" : "none"; + }); + } + + function setupWeightingFactor() { + document.getElementById("weighting-factor").addEventListener("change", function() { + const showWeights = this.value === "static"; + document.querySelectorAll(".weight-input").forEach(input => { + input.style.display = showWeights ? "inline-block" : "none"; + }); + }); + } + + function setupWeightValidation() { + document.querySelectorAll(".weight-input").forEach(input => { + input.addEventListener("input", validateWeights); + }); + } + + function validateWeights() { + let totalWeight = 0; + document.querySelectorAll(".weight-input").forEach(input => { + const checkbox = input.previousElementSibling.previousElementSibling; + if (checkbox.checked && input.style.display !== "none" && input.value) { + totalWeight += parseFloat(input.value); + } + }); + document.getElementById("weight-warning").style.display = totalWeight > 1 ? "block" : "none"; + } + + function setupInitialReputation() { + document.getElementById("initial-reputation").addEventListener("blur", function() { + const min = parseFloat(this.min); + const max = parseFloat(this.max); + const value = parseFloat(this.value); + + if (value < min) { + this.value = min; + } else if (value > max) { + this.value = max; + } + }); + } + + function getReputationConfig() { + return { + enabled: document.getElementById("reputationSwitch").checked, + initialReputation: parseFloat(document.getElementById("initial-reputation").value), + weightingFactor: document.getElementById("weighting-factor").value, + metrics: { + modelSimilarity: { + enabled: document.getElementById("model-similarity").checked, + weight: parseFloat(document.getElementById("weight-model-similarity").value) + }, + numMessages: { + enabled: document.getElementById("num-messages").checked, + weight: parseFloat(document.getElementById("weight-num-messages").value) + }, + modelArrivalLatency: { + enabled: document.getElementById("model-arrival-latency").checked, + weight: parseFloat(document.getElementById("weight-model-arrival-latency").value) + }, + fractionParametersChanged: { + enabled: document.getElementById("fraction-parameters-changed").checked, + weight: parseFloat(document.getElementById("weight-fraction-parameters-changed").value) + } + } + }; + } + + function setReputationConfig(config) { + if (!config) return; + + // Set reputation enabled/disabled + document.getElementById("reputationSwitch").checked = config.enabled; + document.getElementById("reputation-metrics").style.display = config.enabled ? "block" : "none"; + document.getElementById("reputation-settings").style.display = config.enabled ? "block" : "none"; + document.getElementById("weighting-settings").style.display = config.enabled ? "block" : "none"; + + // Set initial reputation + document.getElementById("initial-reputation").value = config.initialReputation || 0.6; + + // Set weighting factor + document.getElementById("weighting-factor").value = config.weightingFactor || "dynamic"; + const showWeights = config.weightingFactor === "static"; + document.querySelectorAll(".weight-input").forEach(input => { + input.style.display = showWeights ? "inline-block" : "none"; + }); + + // Set metrics + if (config.metrics) { + // Model Similarity + document.getElementById("model-similarity").checked = config.metrics.modelSimilarity?.enabled || false; + document.getElementById("weight-model-similarity").value = config.metrics.modelSimilarity?.weight || 0; + + // Number of Messages + document.getElementById("num-messages").checked = config.metrics.numMessages?.enabled || false; + document.getElementById("weight-num-messages").value = config.metrics.numMessages?.weight || 0; + + // Model Arrival Latency + document.getElementById("model-arrival-latency").checked = config.metrics.modelArrivalLatency?.enabled || false; + document.getElementById("weight-model-arrival-latency").value = config.metrics.modelArrivalLatency?.weight || 0; + + // Fraction Parameters Changed + document.getElementById("fraction-parameters-changed").checked = config.metrics.fractionParametersChanged?.enabled || false; + document.getElementById("weight-fraction-parameters-changed").value = config.metrics.fractionParametersChanged?.weight || 0; + } + + // Validate weights + validateWeights(); + } + + function resetReputationConfig() { + // Reset to default values + document.getElementById("reputationSwitch").checked = false; + document.getElementById("reputation-metrics").style.display = "none"; + document.getElementById("reputation-settings").style.display = "none"; + document.getElementById("weighting-settings").style.display = "none"; + document.getElementById("initial-reputation").value = "0.6"; + document.getElementById("weighting-factor").value = "dynamic"; + document.getElementById("weight-warning").style.display = "none"; + + // Reset metrics + document.getElementById("model-similarity").checked = false; + document.getElementById("weight-model-similarity").value = "0"; + document.getElementById("num-messages").checked = false; + document.getElementById("weight-num-messages").value = "0"; + document.getElementById("model-arrival-latency").checked = false; + document.getElementById("weight-model-arrival-latency").value = "0"; + document.getElementById("fraction-parameters-changed").checked = false; + document.getElementById("weight-fraction-parameters-changed").value = "0"; + + // Hide weight inputs + document.querySelectorAll(".weight-input").forEach(input => { + input.style.display = "none"; + }); + } + + return { + initializeReputationSystem, + getReputationConfig, + setReputationConfig, + resetReputationConfig + }; +})(); + +export default ReputationManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/scenario.js b/nebula/frontend/static/js/deployment/scenario.js new file mode 100644 index 000000000..1c071a0a8 --- /dev/null +++ b/nebula/frontend/static/js/deployment/scenario.js @@ -0,0 +1,335 @@ +// Scenario Management Module +const ScenarioManager = (function() { + let scenariosList = []; + let actual_scenario = 0; + + // Initialize scenarios from session storage + function initializeScenarios() { + // Clear session storage + sessionStorage.removeItem("ScenarioList"); + + // Reset the scenarios list + scenariosList = []; + actual_scenario = 0; + + // Clear all fields and reset modules + clearFields(); + + // Update UI + updateScenariosPosition(true); + } + + // Don't call initializeScenarios immediately + // It will be called after all modules are initialized + + function collectScenarioData() { + const topologyData = window.TopologyManager.getData(); + const nodes = {}; + const nodes_graph = {}; + + // Convert nodes array to objects with string IDs + topologyData.nodes.forEach(node => { + const nodeId = node.id.toString(); + nodes[nodeId] = { + id: nodeId, + ip: node.ip, + port: node.port, + role: node.role, + malicious: node.malicious, + proxy: node.proxy, + start: node.start, + neighbors: node.neighbors.map(n => n.toString()), + links: node.links, + attacks: [], + attack_params: {}, + with_reputation: false, + mobility: false + }; + nodes_graph[nodeId] = { + id: nodeId, + role: node.role, + malicious: node.malicious, + proxy: node.proxy, + start: node.start + }; + }); + + // Get topology type from select element + const topologyType = document.getElementById('predefined-topology-select').value; + + return { + scenario_title: document.getElementById("scenario-title").value, + scenario_description: document.getElementById("scenario-description").value, + deployment: document.querySelector('input[name="deploymentRadioOptions"]:checked').value, + federation: document.getElementById("federationArchitecture").value, + rounds: parseInt(document.getElementById("rounds").value), + topology: topologyType, + nodes: nodes, + nodes_graph: nodes_graph, + n_nodes: topologyData.nodes.length, + matrix: window.TopologyManager.getMatrix(), + dataset: document.getElementById("datasetSelect").value, + iid: document.getElementById("iidSelect").value === "true", + partition_selection: document.getElementById("partitionSelect").value, + partition_parameter: parseFloat(document.getElementById("partitionParameter").value), + model: document.getElementById("modelSelect").value, + agg_algorithm: document.getElementById("aggregationSelect").value, + logginglevel: document.getElementById("loggingLevel").value === "true", + report_status_data_queue: document.getElementById("reportingSwitch").checked, + epochs: parseInt(document.getElementById("epochs").value), + attacks: window.AttackManager.getAttackConfig().attacks || [], + poisoned_node_percent: window.AttackManager.getAttackConfig().poisoned_node_percent || 0, + poisoned_sample_percent: window.AttackManager.getAttackConfig().poisoned_sample_percent || 0, + poisoned_noise_percent: window.AttackManager.getAttackConfig().poisoned_noise_percent || 0, + attack_params: window.AttackManager.getAttackConfig().attack_params || {}, + with_reputation: window.ReputationManager.getReputationConfig().with_reputation || false, + reputation_metrics: window.ReputationManager.getReputationConfig().reputation_metrics || [], + initial_reputation: window.ReputationManager.getReputationConfig().initial_reputation || 1.0, + weighting_factor: window.ReputationManager.getReputationConfig().weighting_factor || "static", + weight_model_arrival_latency: window.ReputationManager.getReputationConfig().weight_model_arrival_latency || 0.25, + weight_model_similarity: window.ReputationManager.getReputationConfig().weight_model_similarity || 0.25, + weight_num_messages: window.ReputationManager.getReputationConfig().weight_num_messages || 0.25, + weight_fraction_params_changed: window.ReputationManager.getReputationConfig().weight_fraction_params_changed || 0.25, + mobility: window.MobilityManager.getMobilityConfig().enabled || false, + mobility_type: window.MobilityManager.getMobilityConfig().mobilityType || "random", + radius_federation: window.MobilityManager.getMobilityConfig().radiusFederation || 1000, + scheme_mobility: window.MobilityManager.getMobilityConfig().schemeMobility || "random", + round_frequency: window.MobilityManager.getMobilityConfig().roundFrequency || 1, + mobile_participants_percent: window.MobilityManager.getMobilityConfig().mobileParticipantsPercent || 0.5, + random_geo: window.MobilityManager.getMobilityConfig().randomGeo || false, + latitude: window.MobilityManager.getMobilityConfig().location.latitude || 0, + longitude: window.MobilityManager.getMobilityConfig().location.longitude || 0, + random_topology_probability: document.getElementById("random-probability").value || 0.5, + network_subnet: "172.20.0.0/16", + network_gateway: "172.20.0.1", + additional_participants: window.MobilityManager.getMobilityConfig().additionalParticipants || [], + schema_additional_participants: document.getElementById("schemaAdditionalParticipantsSelect").value || "random", + accelerator: "cpu", + gpu_id: [] + }; + } + + function loadScenarioData(scenario) { + if (!scenario) return; + + // Load basic fields + document.getElementById("scenario-title").value = scenario.scenario_title || ""; + document.getElementById("scenario-description").value = scenario.scenario_description || ""; + + // Load deployment + const deploymentRadio = document.querySelector(`input[name="deploymentRadioOptions"][value="${scenario.deployment}"]`); + if (deploymentRadio) deploymentRadio.checked = true; + + // Load architecture and rounds + document.getElementById("federationArchitecture").value = scenario.federation; + document.getElementById("rounds").value = scenario.rounds; + + // Load topology + if (scenario.nodes && scenario.nodes_graph) { + const topologyData = { + nodes: Object.values(scenario.nodes), + links: [] + }; + + // Reconstruct links from the nodes' neighbors + topologyData.nodes.forEach(node => { + if (node.neighbors) { + node.neighbors.forEach(neighborId => { + topologyData.links.push({ + source: node.id, + target: neighborId + }); + }); + } + }); + + window.TopologyManager.setData(topologyData); + } else { + window.TopologyManager.generatePredefinedTopology(); + } + + // Load dataset settings + document.getElementById("datasetSelect").value = scenario.dataset; + document.getElementById("iidSelect").value = scenario.iid ? "true" : "false"; + document.getElementById("partitionSelect").value = scenario.partition_selection; + document.getElementById("partitionParameter").value = scenario.partition_parameter; + + // Load model and aggregation + document.getElementById("modelSelect").value = scenario.model; + document.getElementById("aggregationSelect").value = scenario.agg_algorithm; + + // Load advanced settings + document.getElementById("loggingLevel").value = scenario.logginglevel ? "true" : "false"; + document.getElementById("reportingSwitch").checked = scenario.report_status_data_queue; + document.getElementById("epochs").value = scenario.epochs; + + // Load module configurations + if (scenario.attacks && scenario.attacks.length > 0) { + window.AttackManager.setAttackConfig({ + attacks: scenario.attacks, + poisoned_node_percent: scenario.poisoned_node_percent, + poisoned_sample_percent: scenario.poisoned_sample_percent, + poisoned_noise_percent: scenario.poisoned_noise_percent, + attack_params: scenario.attack_params + }); + } + if (scenario.mobility) { + window.MobilityManager.setMobilityConfig({ + enabled: scenario.mobility, + mobilityType: scenario.mobility_type, + radiusFederation: scenario.radius_federation, + schemeMobility: scenario.scheme_mobility, + roundFrequency: scenario.round_frequency, + mobileParticipantsPercent: scenario.mobile_participants_percent, + randomGeo: scenario.random_geo, + location: { + latitude: scenario.latitude, + longitude: scenario.longitude + }, + additionalParticipants: scenario.additional_participants + }); + } + if (scenario.with_reputation) { + window.ReputationManager.setReputationConfig({ + with_reputation: scenario.with_reputation, + reputation_metrics: scenario.reputation_metrics, + initial_reputation: scenario.initial_reputation, + weighting_factor: scenario.weighting_factor, + weight_model_arrival_latency: scenario.weight_model_arrival_latency, + weight_model_similarity: scenario.weight_model_similarity, + weight_num_messages: scenario.weight_num_messages, + weight_fraction_params_changed: scenario.weight_fraction_params_changed + }); + } + + // Trigger necessary events + document.getElementById("federationArchitecture").dispatchEvent(new Event('change')); + document.getElementById("datasetSelect").dispatchEvent(new Event('change')); + document.getElementById("iidSelect").dispatchEvent(new Event('change')); + } + + function saveScenario() { + const scenarioData = collectScenarioData(); + scenariosList.push(scenarioData); + actual_scenario = scenariosList.length - 1; + sessionStorage.setItem("ScenarioList", JSON.stringify(scenariosList)); + updateScenariosPosition(); + } + + function deleteScenario() { + if (scenariosList.length === 0) return; + + scenariosList.splice(actual_scenario, 1); + if (actual_scenario >= scenariosList.length) { + actual_scenario = Math.max(0, scenariosList.length - 1); + } + + if (scenariosList.length > 0) { + loadScenarioData(scenariosList[actual_scenario]); + } else { + clearFields(); + } + + sessionStorage.setItem("ScenarioList", JSON.stringify(scenariosList)); + updateScenariosPosition(scenariosList.length === 0); + } + + function replaceScenario() { + if (actual_scenario < 0 || actual_scenario >= scenariosList.length) return; + + const scenarioData = collectScenarioData(); + scenariosList[actual_scenario] = scenarioData; + sessionStorage.setItem("ScenarioList", JSON.stringify(scenariosList)); + } + + function updateScenariosPosition(isEmptyScenario = false) { + const container = document.getElementById("scenarios-position"); + if (!container) return; + + // Clear existing content + container.innerHTML = ''; + + if (isEmptyScenario) { + container.innerHTML = 'No scenarios'; + return; + } + + // Create a single span for all scenarios + const span = document.createElement("span"); + span.style.margin = "0 10px"; + + // Create the scenario indicators + const indicators = scenariosList.map((_, index) => + index === actual_scenario ? `●` : `○` + ).join(' '); + + span.textContent = indicators; + container.appendChild(span); + } + + function clearFields() { + // Reset form fields to default values + document.getElementById("scenario-title").value = ""; + document.getElementById("scenario-description").value = ""; + document.getElementById("docker-radio").checked = true; + document.getElementById("federationArchitecture").value = "DFL"; + document.getElementById("rounds").value = "10"; + document.getElementById("custom-topology-btn").checked = true; + document.getElementById("predefined-topology").style.display = "none"; + document.getElementById("datasetSelect").value = "MNIST"; + document.getElementById("iidSelect").value = "false"; + document.getElementById("partitionSelect").value = "dirichlet"; + document.getElementById("partitionParameter").value = "0.5"; + document.getElementById("modelSelect").value = "MLP"; + document.getElementById("aggregationSelect").value = "FedAvg"; + document.getElementById("loggingLevel").value = "false"; + document.getElementById("reportingSwitch").checked = true; + document.getElementById("epochs").value = "1"; + + // Reset modules + if (window.TopologyManager) { + window.TopologyManager.generatePredefinedTopology(); + } + if (window.AttackManager) { + window.AttackManager.resetAttackConfig(); + } + if (window.MobilityManager) { + window.MobilityManager.resetMobilityConfig(); + } + if (window.ReputationManager) { + window.ReputationManager.resetReputationConfig(); + } + + // Trigger necessary events + document.getElementById("federationArchitecture").dispatchEvent(new Event('change')); + document.getElementById("datasetSelect").dispatchEvent(new Event('change')); + document.getElementById("iidSelect").dispatchEvent(new Event('change')); + } + + return { + saveScenario, + deleteScenario, + replaceScenario, + loadScenarioData, + clearFields, + updateScenariosPosition, + initializeScenarios, + getScenariosList: () => scenariosList, + getActualScenario: () => actual_scenario, + setActualScenario: (index) => { + actual_scenario = index; + if (scenariosList[index]) { + loadScenarioData(scenariosList[index]); + } + }, + setScenariosList: (list) => { + scenariosList = list; + if (list.length > 0) { + actual_scenario = 0; + loadScenarioData(list[0]); + } + } + }; +})(); + +export default ScenarioManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/topology.js b/nebula/frontend/static/js/deployment/topology.js new file mode 100644 index 000000000..0f0ac9add --- /dev/null +++ b/nebula/frontend/static/js/deployment/topology.js @@ -0,0 +1,637 @@ +// Topology Management Module + +const TopologyManager = (function() { + let gData = { + nodes: [], + links: [] + }; + let Graph = null; + let selectedNodes = new Set(); + + function initializeGraph(containerId, width, height) { + setupTopologyListeners(); + generatePredefinedTopology(); + } + + function setupTopologyListeners() { + const topologySelect = document.getElementById('predefined-topology-select'); + const nodesInput = document.getElementById('predefined-topology-nodes'); + const probabilitySelect = document.getElementById('random-probability'); + const randomOptions = document.getElementById('random-topology-options'); + const predefinedTopology = document.getElementById('predefined-topology'); + const customTopologyBtn = document.getElementById('custom-topology-btn'); + const predefinedTopologyBtn = document.getElementById('predefined-topology-btn'); + + // Set default topology + topologySelect.value = 'Fully'; + nodesInput.value = '3'; + predefinedTopologyBtn.checked = true; + predefinedTopology.style.display = 'block'; + + // Add radio button listeners + customTopologyBtn.addEventListener('change', () => { + predefinedTopology.style.display = 'none'; + }); + + predefinedTopologyBtn.addEventListener('change', () => { + predefinedTopology.style.display = 'block'; + generatePredefinedTopology(); + }); + + // Add federation architecture change listener + document.getElementById('federationArchitecture').addEventListener('change', function() { + const federationType = this.value; + const topologySelect = document.getElementById('predefined-topology-select'); + + if (federationType === 'CFL') { + // For CFL, only allow Star topology + topologySelect.value = 'Star'; + topologySelect.disabled = true; + predefinedTopologyBtn.checked = true; + predefinedTopology.style.display = 'block'; + customTopologyBtn.disabled = true; + } else { + // For DFL and SDFL, allow all topologies + topologySelect.disabled = false; + customTopologyBtn.disabled = false; + } + + generatePredefinedTopology(); + }); + + topologySelect.addEventListener('change', () => { + if (topologySelect.value === 'Random') { + randomOptions.style.display = 'block'; + } else { + randomOptions.style.display = 'none'; + } + generatePredefinedTopology(); + }); + + nodesInput.addEventListener('input', () => { + generatePredefinedTopology(); + }); + + probabilitySelect.addEventListener('change', () => { + if (topologySelect.value === 'Random') { + generatePredefinedTopology(); + } + }); + } + + function generatePredefinedTopology() { + const topologyType = document.getElementById('predefined-topology-select').value; + const N = parseInt(document.getElementById('predefined-topology-nodes').value) || 3; + let probability = 0.5; // default value + + if (topologyType === 'Random') { + const probSelect = document.getElementById('random-probability'); + probability = parseFloat(probSelect.value); + } + + // Create nodes with roles based on topology type + gData.nodes = [...Array(N).keys()].map(i => { + let role; + switch(topologyType) { + case 'Fully': + role = "aggregator"; + break; + case 'Star': + role = i === 0 ? "server" : "trainer"; + break; + case 'Ring': + role = "aggregator"; + break; + case 'Random': + role = "aggregator"; + break; + default: + role = i === 0 ? "aggregator" : "trainer"; + } + + return { + id: i, + ip: "127.0.0.1", + port: (45000 + i).toString(), + role: role, + malicious: false, + proxy: false, + start: (i === 0), + neighbors: [], + links: [], + }; + }); + + // Create links based on topology type + gData.links = []; + switch(topologyType) { + case 'Fully': + for (let i = 0; i < N; i++) { + for (let j = i + 1; j < N; j++) { + gData.links.push({ source: i, target: j }); + } + } + break; + case 'Ring': + for (let i = 0; i < N; i++) { + gData.links.push({ source: i, target: (i + 1) % N }); + } + break; + case 'Star': + for (let i = 1; i < N; i++) { + gData.links.push({ source: 0, target: i }); + } + break; + case 'Random': + for (let i = 0; i < N; i++) { + for (let j = i + 1; j < N; j++) { + if (Math.random() < probability) { + gData.links.push({ source: i, target: j }); + } + } + } + break; + } + + // After generating topology, assign roles based on federation architecture + assignRolesByFederationArchitecture(); + + // Update graph visualization + if (Graph) { + updateGraph(); + } else { + const containerId = '3d-graph'; + Graph = ForceGraph3D()(document.getElementById(containerId)) + .graphData(gData) + .nodeThreeObject(createNodeObject) + .showNavInfo(false) + .width(document.getElementById(containerId).offsetWidth) + .height(document.getElementById(containerId).offsetHeight) + .backgroundColor('#ffffff') + .nodeLabel(node => `

ID: ${node.id} | Role: ${node.role} | ${node.malicious ? 'Malicious' : 'Honest'}

`) + .onNodeRightClick(handleNodeRightClick) + .onNodeClick(handleNodeClick) + .onBackgroundClick(handleBackgroundClick) + .onLinkClick(handleLinkClick) + .linkColor(link => link.color || '#999') + .linkWidth(2) + .linkOpacity(0.6) + .linkDirectionalParticles(2) + .linkDirectionalParticleWidth(2) + .linkDirectionalParticleSpeed(0.005) + .linkCurvature(0.25) + .linkDirectionalParticleResolution(2) + .linkDirectionalParticleColor(() => '#ff0000'); + } + + // Update neighbors + updateNeighbors(); + // Emit event when graph data changes + emitGraphDataUpdated(); + } + + function updateGraphData(newData) { + gData = newData; + Graph.graphData(gData); + emitGraphDataUpdated(); + } + + function emitGraphDataUpdated() { + document.dispatchEvent(new CustomEvent('graphDataUpdated')); + } + + function handleNodeRightClick(node, event) { + const dropdown = document.getElementById("node-dropdown"); + dropdown.innerHTML = ""; + dropdown.style.display = "block"; + dropdown.style.left = event.clientX + "px"; + dropdown.style.top = event.clientY + "px"; + dropdown.style.position = "absolute"; + dropdown.style.zIndex = "1000"; + dropdown.style.backgroundColor = "white"; + dropdown.style.border = "1px solid black"; + dropdown.style.padding = "10px"; + dropdown.style.borderRadius = "5px"; + dropdown.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)"; + dropdown.setAttribute("data-id", node.id); + + const title = document.createElement("h5"); + title.innerHTML = "Node " + node.id; + dropdown.appendChild(title); + + createDropdownOptions(dropdown, node); + } + + function createDropdownOptions(dropdown, node) { + const options = [ + { + label: "Add node", + icon: "fa-plus", + handler: () => addNode(node), + condition: () => document.getElementById("federationArchitecture").value !== "CFL" + }, + { + label: "Remove node", + icon: "fa-minus", + handler: () => removeNode(node), + condition: () => document.getElementById("federationArchitecture").value !== "CFL" + }, + { + label: "Change role", + icon: "fa-refresh", + handler: () => changeRole(node), + condition: () => document.getElementById("federationArchitecture").value !== "CFL" + }, + { + label: "Change malicious", + icon: "fa-exclamation-triangle", + handler: () => changeMalicious(node), + condition: () => true + }, + { + label: "Change proxy", + icon: "fa-exchange", + handler: () => changeProxy(node), + condition: () => document.getElementById("federationArchitecture").value !== "CFL" + } + ]; + + options.forEach(option => { + if (option.condition()) { + const element = createDropdownElement(option.label, option.icon, option.handler); + dropdown.appendChild(element); + } + }); + } + + function createDropdownElement(label, icon, handler) { + const element = document.createElement("a"); + element.innerHTML = ` ${label}`; + element.style.display = "block"; + element.style.cursor = "pointer"; + element.style.padding = "5px"; + element.style.borderRadius = "5px"; + element.style.marginBottom = "5px"; + element.classList.add("dropdown-item"); + element.addEventListener("click", handler); + return element; + } + + function handleNodeClick(node) { + if (!selectedNodes.has(node)) { + selectedNodes.add(node); + } else { + selectedNodes.delete(node); + } + + // Force graph update to show color change + Graph.nodeThreeObject(node => createNodeObject(node)); + + if (selectedNodes.size === 2) { + const [a, b] = selectedNodes; + if (document.getElementById("federationArchitecture").value !== "CFL") { + const link = { source: a, target: b }; + a.neighbors.push(b.id); + b.neighbors.push(a.id); + gData.links.push(link); + } + selectedNodes.clear(); + // Force another update after clearing selection + Graph.nodeThreeObject(node => createNodeObject(node)); + } + updateGraph(); + } + + function handleLinkClick(link) { + if (document.getElementById("federationArchitecture").value === "CFL") { + return; + } + + // Find and remove both directional links + const source = typeof link.source === 'object' ? link.source.id : link.source; + const target = typeof link.target === 'object' ? link.target.id : link.target; + + gData.links = gData.links.filter(l => { + const lSource = typeof l.source === 'object' ? l.source.id : l.source; + const lTarget = typeof l.target === 'object' ? l.target.id : l.target; + return !((lSource === source && lTarget === target) || (lSource === target && lTarget === source)); + }); + + // Remove from neighbors + gData.nodes[source].neighbors = gData.nodes[source].neighbors.filter(id => id !== target); + gData.nodes[target].neighbors = gData.nodes[target].neighbors.filter(id => id !== source); + + updateGraph(); + } + + function addNode(sourceNode) { + document.getElementById("custom-topology-btn").checked = true; + document.getElementById("predefined-topology").style.display = "none"; + + const newNode = { + id: gData.nodes.length, + ip: "127.0.0.1", + port: "45000", + role: 'aggregator', + malicious: false, + proxy: false, + start: false, + neighbors: [sourceNode.id], + links: [] + }; + + sourceNode.neighbors.push(newNode.id); + gData.nodes.push(newNode); + gData.links.push({ source: newNode.id, target: sourceNode.id }); + updateGraph(); + } + + function removeNode(node) { + if (gData.nodes.length <= 1) return; + + document.getElementById("custom-topology-btn").checked = true; + document.getElementById("predefined-topology").style.display = "none"; + + // Remove links connected to this node + gData.links = gData.links.filter(l => + l.source.id !== node.id && l.target.id !== node.id + ); + + // Remove node and update IDs + gData.nodes = gData.nodes.filter(n => n.id !== node.id); + gData.nodes.forEach((n, idx) => { + n.id = idx; + n.neighbors = n.neighbors.filter(id => id !== node.id) + .map(id => id > node.id ? id - 1 : id); + }); + + updateGraph(); + } + + function changeRole(node) { + node.role = node.role === 'trainer' ? 'aggregator' : 'trainer'; + updateGraph(); + } + + function changeMalicious(node) { + document.getElementById("malicious-nodes-select").value = "Manual"; + document.getElementById("malicious-nodes-select").dispatchEvent(new Event('change')); + node.malicious = !node.malicious; + // Force complete graph update + if (Graph) { + Graph.nodeThreeObject(node => createNodeObject(node)); + Graph.graphData(gData); + } + updateGraph(); + } + + function changeProxy(node) { + node.proxy = !node.proxy; + // Force complete graph update + if (Graph) { + Graph.nodeThreeObject(node => createNodeObject(node)); + Graph.graphData(gData); + } + updateGraph(); + } + + function createNodeObject(node) { + let geometry; + let main_color; + + if (node.malicious) { + geometry = new THREE.TorusGeometry(5, 2, 16, 100); + main_color = "#000000"; + } else { + switch (node.role) { + case 'aggregator': + geometry = new THREE.SphereGeometry(5); + main_color = "#d95f02"; + break; + case 'trainer': + geometry = new THREE.ConeGeometry(5, 12); + main_color = "#7570b3"; + break; + case 'server': + geometry = new THREE.BoxGeometry(10, 10, 10); + main_color = "#1b9e77"; + break; + default: + break; + } + } + + const isSelected = selectedNodes.has(node); + const color = isSelected ? '#ff0000' : main_color; + + const mesh = new THREE.Mesh( + geometry, + new THREE.MeshLambertMaterial({ + color: color, + transparent: false, + opacity: 0.75 + }) + ); + + if (node.proxy) { + // Add proxy indicator + const sprite = createProxySprite(); + mesh.add(sprite); + } + + return mesh; + } + + function createProxySprite() { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + context.font = '80px Arial'; + context.fillText('PROXY', 0, 70); + + const texture = new THREE.CanvasTexture(canvas); + const spriteMaterial = new THREE.SpriteMaterial({ map: texture }); + const sprite = new THREE.Sprite(spriteMaterial); + sprite.scale.set(10, 10 * 0.7, 5); + sprite.position.set(0, 5, 0); + + return sprite; + } + + function updateGraph() { + gDataUpdate(); + if (Graph) { + Graph.graphData(gData); + // Update link visualization + Graph.linkColor(link => link.color || '#999') + .linkWidth(2) + .linkOpacity(0.6) + .linkDirectionalParticles(2) + .linkDirectionalParticleWidth(3) + .linkDirectionalParticleSpeed(0.01) + .linkCurvature(0.25) + .linkDirectionalParticleResolution(2) + .linkDirectionalParticleColor(() => '#ff0000'); + } + } + + function gDataUpdate() { + // Remove duplicated links + removeDuplicateLinks(); + // Update neighbors + updateNeighbors(); + // Update IPs and ports + updateIPsAndPorts(); + } + + function removeDuplicateLinks() { + for (let i = 0; i < gData.links.length; i++) { + for (let j = i + 1; j < gData.links.length; j++) { + if ((gData.links[i].source === gData.links[j].source && gData.links[i].target === gData.links[j].target) || + (gData.links[i].source === gData.links[j].target && gData.links[i].target === gData.links[j].source)) { + gData.links.splice(j, 1); + } + } + } + } + + function updateNeighbors() { + gData.links.forEach(link => { + for (let node of gData.nodes) { + if (node.id === link.source) { + node.neighbors.push(link.target); + } + if (node.id === link.target) { + node.neighbors.push(link.source); + } + } + }); + + // Clean up neighbors + for (let node of gData.nodes) { + node.neighbors = [...new Set(node.neighbors)].filter(id => id !== node.id); + } + } + + function updateIPsAndPorts() { + const isProcess = document.getElementById("process-radio").checked; + const baseIP = "192.168.50"; + + gData.nodes.forEach((node, index) => { + node.ip = isProcess ? "127.0.0.1" : `${baseIP}.${index + 2}`; + node.port = (45001 + index).toString(); + }); + } + + function getMatrix() { + const matrix = Array(gData.nodes.length).fill().map(() => Array(gData.nodes.length).fill(0)); + + gData.links.forEach(link => { + const source = typeof link.source === 'object' ? link.source.id : link.source; + const target = typeof link.target === 'object' ? link.target.id : link.target; + matrix[source][target] = 1; + matrix[target][source] = 1; + }); + + // Ensure diagonal is 0 + for (let i = 0; i < matrix.length; i++) { + matrix[i][i] = 0; + } + + return matrix; + } + + function handleBackgroundClick() { + // Clear selected nodes + selectedNodes.clear(); + // Hide node dropdown if visible + const dropdown = document.getElementById("node-dropdown"); + if (dropdown) { + dropdown.style.display = "none"; + } + // Update graph to reflect changes + updateGraph(); + } + + function assignRolesByFederationArchitecture() { + const federationType = document.getElementById("federationArchitecture").value; + const nodes = gData.nodes; + + if (nodes.length === 0) return; + + switch (federationType) { + case "CFL": + // First node as server, rest as trainers + nodes[0].role = "server"; + for (let i = 1; i < nodes.length; i++) { + nodes[i].role = "trainer"; + } + break; + + case "SDFL": + // All as trainers except one random node as aggregator + const randomIndex = Math.floor(Math.random() * nodes.length); + for (let i = 0; i < nodes.length; i++) { + nodes[i].role = i === randomIndex ? "aggregator" : "trainer"; + } + break; + + case "DFL": + // All as aggregators + for (let i = 0; i < nodes.length; i++) { + nodes[i].role = "aggregator"; + } + break; + } + + // Force complete graph update + if (Graph) { + Graph.nodeThreeObject(node => createNodeObject(node)); + Graph.graphData(gData); + } + updateGraph(); + } + + // Add event listener for federation architecture changes + document.getElementById("federationArchitecture").addEventListener("change", function() { + assignRolesByFederationArchitecture(); + }); + + return { + initializeGraph, + updateGraphData, + getGraph: () => Graph, + getData: () => gData, + setData: (data) => { + // Ensure data has the required structure + if (!data || !data.nodes || !data.links) { + // If data is invalid, generate a new predefined topology + generatePredefinedTopology(); + return; + } + + // Ensure each node has the required properties + data.nodes = data.nodes.map(node => ({ + id: node.id, + role: node.role || 'trainer', + malicious: node.malicious || false, + proxy: node.proxy || false, + neighbors: node.neighbors || [], + links: node.links || [] + })); + + // Ensure each link has the required properties + data.links = data.links.map(link => ({ + source: link.source, + target: link.target + })); + + gData = data; + updateGraph(); + }, + getMatrix, + generatePredefinedTopology, + updateGraph + }; +})(); + +export default TopologyManager; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/ui-controls.js b/nebula/frontend/static/js/deployment/ui-controls.js new file mode 100644 index 000000000..c9db4f2f4 --- /dev/null +++ b/nebula/frontend/static/js/deployment/ui-controls.js @@ -0,0 +1,627 @@ +// UI Controls Module +const UIControls = (function() { + function initializeUIControls() { + setupModeButton(); + setupPartitionControls(); + setupMethodTip(); + setupReputationControls(); + setupActionButtons(); + setupDeploymentButtons(); + setupParticipantDisplay(); + setupParticipantModal(); + setupConfigButtons(); + // Initialize help icons + window.HelpContent.initializePopovers(); + } + + function setupModeButton() { + const modeBtn = document.getElementById('mode-btn'); + const expertContainer = document.getElementById('expert-container'); + const loggingLevel = document.getElementById('loggingLevel'); + + if (modeBtn) { + modeBtn.addEventListener('click', function() { + const isAdvancedMode = modeBtn.innerHTML.trim() === "Advanced mode"; + + if (isAdvancedMode) { + // Switch to advanced mode + modeBtn.innerHTML = "User mode"; + modeBtn.classList.remove("btn-dark"); + modeBtn.classList.add("btn-light"); + expertContainer.style.display = "block"; + expertContainer.style.visibility = "visible"; + expertContainer.style.opacity = "1"; + expertContainer.style.transition = "all 0.5s ease-in-out"; + loggingLevel.value = "true"; + } else { + // Switch back to user mode + resetModeBtn(); + } + }); + } + } + + function resetModeBtn() { + const modeBtn = document.getElementById('mode-btn'); + const expertContainer = document.getElementById('expert-container'); + const loggingLevel = document.getElementById('loggingLevel'); + + modeBtn.innerHTML = "Advanced mode"; + modeBtn.classList.remove("btn-light"); + modeBtn.classList.add("btn-dark"); + expertContainer.style.display = "none"; + expertContainer.style.visibility = "hidden"; + expertContainer.style.opacity = "0"; + expertContainer.style.transition = "all 0.5s ease-in-out"; + loggingLevel.value = "false"; + } + + function setupPartitionControls() { + const iidSelect = document.getElementById("iidSelect"); + const partitionSelect = document.getElementById("partitionSelect"); + const partitionBlock = document.getElementById("partitionBlock"); + const partitionParameter = document.getElementById("partitionParameter"); + + iidSelect.addEventListener("change", function() { + if (iidSelect.value === "false") { + // Set up for non-IID + partitionSelect.options[0].selected = true; + partitionSelect.options[2].selected = false; + partitionSelect.options[0].disabled = false; + partitionSelect.options[1].disabled = false; + partitionSelect.options[2].disabled = true; + partitionSelect.options[3].disabled = true; + + partitionSelect.options[0].style.display = "block"; + partitionSelect.options[1].style.display = "block"; + partitionSelect.options[2].style.display = "none"; + partitionSelect.options[3].style.display = "none"; + partitionBlock.style.display = "block"; + } else { + // Set up for IID + partitionSelect.options[0].selected = false; + partitionSelect.options[2].selected = true; + partitionSelect.options[0].disabled = true; + partitionSelect.options[1].disabled = true; + partitionSelect.options[2].disabled = false; + partitionSelect.options[3].disabled = false; + + partitionSelect.options[0].style.display = "none"; + partitionSelect.options[1].style.display = "none"; + partitionSelect.options[2].style.display = "block"; + partitionSelect.options[3].style.display = "block"; + partitionBlock.style.display = "none"; + } + }); + + partitionSelect.addEventListener("change", function() { + switch(partitionSelect.value) { + case "balancediid": + partitionBlock.style.display = "none"; + partitionParameter.value = "0.0"; + break; + case "unbalancediid": + partitionBlock.style.display = "block"; + partitionParameter.value = "2"; + partitionParameter.step = "0.1"; + partitionParameter.min = "1"; + break; + case "dirichlet": + partitionBlock.style.display = "block"; + partitionParameter.value = "0.5"; + partitionParameter.step = "0.1"; + partitionParameter.min = "0.1"; + break; + case "percentage": + partitionBlock.style.display = "block"; + partitionParameter.value = "50"; + partitionParameter.step = "1"; + partitionParameter.min = "10"; + partitionParameter.max = "100"; + break; + } + }); + } + + function setupMethodTip() { + const methodtip = document.getElementById('methodtip'); + const methodtipImage = document.getElementById('methodtipImage'); + const partitionSelect = document.getElementById('partitionSelect'); + + methodtip.addEventListener('mouseover', function() { + const imageMap = { + "dirichlet": "dirichlet_noniid.png", + "percent": "percentage.png", + "balancediid": "balancediid.png", + "unbalancediid": "unbalanceiid.png" + }; + + const imageName = imageMap[partitionSelect.value]; + if (imageName) { + methodtipImage.src = `/static/images/${imageName}`; + methodtipImage.style.display = "block"; + } + }); + + methodtip.addEventListener('mouseout', function() { + methodtipImage.style.display = "none"; + }); + } + + function setupReputationControls() { + const reputationSwitch = document.getElementById("reputationSwitch"); + const initialReputation = document.getElementById("initial-reputation"); + const weightingFactor = document.getElementById("weighting-factor"); + const weightInputs = document.querySelectorAll(".weight-input"); + + reputationSwitch.addEventListener("change", function() { + const elements = ["reputation-metrics", "reputation-settings", "weighting-settings"]; + elements.forEach(id => { + document.getElementById(id).style.display = this.checked ? "block" : "none"; + }); + }); + + initialReputation.addEventListener("blur", function() { + const min = parseFloat(this.min); + const max = parseFloat(this.max); + const value = parseFloat(this.value); + + if (value < min) this.value = min; + else if (value > max) this.value = max; + }); + + weightingFactor.addEventListener("change", function() { + const showWeights = this.value === "static"; + weightInputs.forEach(input => { + input.style.display = showWeights ? "inline-block" : "none"; + }); + }); + + weightInputs.forEach(input => { + input.addEventListener("input", validateWeights); + }); + } + + function validateWeights() { + let totalWeight = 0; + document.querySelectorAll(".weight-input").forEach(input => { + const checkbox = input.previousElementSibling.previousElementSibling; + if (checkbox.checked && input.style.display !== "none" && input.value) { + totalWeight += parseFloat(input.value); + } + }); + document.getElementById("weight-warning").style.display = totalWeight > 1 ? "block" : "none"; + } + + function setupActionButtons() { + // Add button + const addBtn = document.getElementById('add-btn'); + if (addBtn) { + addBtn.addEventListener('click', function() { + window.ScenarioManager.saveScenario(); + updateButtonVisibility(); + }); + } + + // Delete button + const delBtn = document.getElementById('del-btn'); + if (delBtn) { + delBtn.addEventListener('click', function() { + window.ScenarioManager.deleteScenario(); + if (window.ScenarioManager.getScenariosList().length < 1) { + window.ScenarioManager.clearFields(); + window.ScenarioManager.updateScenariosPosition(true); + updateButtonVisibility(true); + } else { + window.ScenarioManager.updateScenariosPosition(); + updateButtonVisibility(); + } + }); + } + + // Previous button + const prevBtn = document.getElementById('prev-btn'); + if (prevBtn) { + prevBtn.addEventListener('click', function() { + window.ScenarioManager.replaceScenario(); + window.ScenarioManager.setActualScenario(window.ScenarioManager.getActualScenario() - 1); + window.ScenarioManager.updateScenariosPosition(); + updateButtonVisibility(); + }); + } + + // Next button + const nextBtn = document.getElementById('next-btn'); + if (nextBtn) { + nextBtn.addEventListener('click', function() { + window.ScenarioManager.replaceScenario(); + window.ScenarioManager.setActualScenario(window.ScenarioManager.getActualScenario() + 1); + window.ScenarioManager.updateScenariosPosition(); + updateButtonVisibility(); + }); + } + } + + function updateButtonVisibility(isEmptyScenario = false) { + const prevBtn = document.getElementById("prev-btn"); + const nextBtn = document.getElementById("next-btn"); + const addBtn = document.getElementById("add-btn"); + const delBtn = document.getElementById("del-btn"); + const runBtn = document.getElementById("run-btn"); + + if (isEmptyScenario) { + prevBtn.style.display = "none"; + nextBtn.style.display = "none"; + delBtn.style.display = "none"; + runBtn.disabled = true; + addBtn.style.display = "inline-block"; + return; + } + + const scenarioCount = window.ScenarioManager.getScenariosList().length; + const currentScenario = window.ScenarioManager.getActualScenario(); + + prevBtn.style.display = currentScenario > 0 ? "inline-block" : "none"; + nextBtn.style.display = currentScenario < scenarioCount - 1 ? "inline-block" : "none"; + addBtn.style.display = currentScenario === scenarioCount - 1 ? "inline-block" : "none"; + delBtn.style.display = "inline-block"; + runBtn.disabled = false; + } + + function setupDeploymentButtons() { + const runBtn = document.getElementById('run-btn'); + if (runBtn) { + runBtn.addEventListener('click', handleDeployment); + } + } + + async function handleDeployment() { + const confirmModal = document.getElementById('confirm-modal'); + const confirmModalBody = document.getElementById('confirm-modal-body'); + const yesButton = document.getElementById("yes-button"); + + if (!document.querySelector(".participant-started")) { + confirmModalBody.innerHTML = 'Please select one "start" participant for the scenario'; + yesButton.disabled = true; + const modal = new bootstrap.Modal(confirmModal); + modal.show(); + return; + } + + const deploymentOption = document.querySelector('input[name="deploymentRadioOptions"]:checked'); + confirmModalBody.innerHTML = `Are you sure you want to run the scenario? +

The scenario will be deployed using the selected deployment option: ${deploymentOption.value}

+

Warning: you will stop the running scenario and start a new one

`; + yesButton.disabled = false; + + const modal = new bootstrap.Modal(confirmModal); + modal.show(); + + yesButton.onclick = async () => { + // If no scenarios exist, save the current one first + if (window.ScenarioManager.getScenariosList().length < 1) { + window.ScenarioManager.saveScenario(); + } else { + window.ScenarioManager.replaceScenario(); + } + + // Ensure all scenarios have a title + window.ScenarioManager.getScenariosList().forEach((scenario, index) => { + if (!scenario.scenario_title) { + scenario.scenario_title = "empty"; + } + if (!scenario.scenario_description) { + scenario.scenario_description = "empty"; + } + }); + + modal.hide(); + document.querySelector(".overlay").style.display = "block"; + document.getElementById("spinner").style.display = "block"; + + try { + const response = await fetch("/platform/dashboard/deployment/run", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(window.ScenarioManager.getScenariosList()) + }); + + if (response.redirected) { + window.location.href = response.url; + } else if (!response.ok) { + handleDeploymentError(response.status); + } + } catch (error) { + console.error('Error:', error); + hideLoadingIndicators(); + handleDeploymentError(500, error); + } finally { + hideLoadingIndicators(); + } + }; + } + + function handleDeploymentError(status, error = null) { + hideLoadingIndicators(); + let errorMessage; + + switch(status) { + case 401: + errorMessage = "You are not authorized to run a scenario. Please log in."; + break; + case 503: + errorMessage = "Not enough resources to run a scenario. Please try again later."; + break; + default: + errorMessage = "An unexpected error occurred. See console for more details."; + } + if (error) { + console.error('Error:', error); + } + showErrorModal(errorMessage); + } + + function showErrorModal(message) { + const infoModal = document.getElementById('info-modal'); + const infoModalBody = document.getElementById('info-modal-body'); + infoModalBody.innerHTML = message; + const modal = new bootstrap.Modal(infoModal); + + // Add event listener for when modal is hidden + infoModal.addEventListener('hidden.bs.modal', function () { + document.querySelector(".overlay").style.display = "none"; + // Remove the modal backdrop + const backdrop = document.querySelector('.modal-backdrop'); + if (backdrop) { + backdrop.remove(); + } + // Remove the modal-open class from body + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; + }); + + modal.show(); + } + + function hideLoadingIndicators() { + document.querySelector(".overlay").style.display = "none"; + document.getElementById("spinner").style.display = "none"; + } + + function setupParticipantDisplay() { + // Initial update of participants + updateParticipantDisplay(); + + // Listen for graph data changes + document.addEventListener('graphDataUpdated', updateParticipantDisplay); + } + + function updateParticipantDisplay() { + const participantItems = document.getElementById("participant-items"); + if (!participantItems) return; + + const graph = window.TopologyManager.getGraph(); + if (!graph) return; + + const nodes = graph.graphData().nodes; + const numberOfNodes = nodes.length; + + // Update the info-participants number + const infoParticipantsNumber = document.getElementById("info-participants-number"); + if (infoParticipantsNumber) { + infoParticipantsNumber.innerHTML = numberOfNodes; + } + + // Clear existing participants + participantItems.innerHTML = ""; + + // Create participant items + nodes.forEach((node, i) => { + const participantItem = createParticipantItem(node, i); + participantItems.appendChild(participantItem); + }); + + // If there is no participant-started, add the class to the first participant + if (document.getElementsByClassName("participant-started").length === 0 && nodes.length > 0) { + const firstStartBtn = document.getElementById("participant-0-start-btn"); + if (firstStartBtn) { + firstStartBtn.classList.add("participant-started"); + firstStartBtn.title = `Participant 0 (start node)`; + nodes[0].start = true; + } + } + } + + function createParticipantItem(node, index) { + const participantItem = document.createElement("div"); + participantItem.classList.add("col-md-2"); + participantItem.classList.add("participant-item"); + + // Create participant image + const participantImg = document.createElement("img"); + participantImg.id = `participant-img-${index}`; + participantImg.setAttribute("data-id", index.toString()); + participantImg.src = "/static/images/device.png"; + participantImg.width = 50; + participantImg.height = 50; + participantImg.style.marginRight = "10px"; + + // Create label + const label = document.createElement("label"); + label.setAttribute("for", `participant-${index}`); + label.setAttribute("data-id", index.toString()); + label.innerHTML = `Participant ${index}`; + label.style.marginRight = "10px"; + + // Create info button + const infoBtn = createInfoButton(node, index); + + // Create start button + const startBtn = createStartButton(node, index); + + // Add all elements to participant item + participantItem.appendChild(participantImg); + participantItem.appendChild(label); + participantItem.appendChild(infoBtn); + participantItem.appendChild(startBtn); + + return participantItem; + } + + function createInfoButton(node, index) { + const infoBtn = document.createElement("button"); + infoBtn.id = `participant-${index}-btn`; + infoBtn.setAttribute("data-id", index.toString()); + infoBtn.type = "button"; + infoBtn.classList.add("btn", "btn-info-participant"); + infoBtn.innerHTML = "Details"; + infoBtn.style.border = "none"; + infoBtn.style.cursor = "pointer"; + + infoBtn.addEventListener("click", () => { + showParticipantDetails(node, index); + }); + + return infoBtn; + } + + function createStartButton(node, index) { + const startBtn = document.createElement("button"); + startBtn.id = `participant-${index}-start-btn`; + startBtn.setAttribute("data-id", index.toString()); + startBtn.type = "button"; + startBtn.classList.add("btn"); + startBtn.innerHTML = "Start"; + startBtn.style.marginLeft = "10px"; + startBtn.style.border = "none"; + startBtn.style.cursor = "pointer"; + + if (node.start) { + startBtn.classList.add("participant-started"); + startBtn.title = `Participant ${index} (start node)`; + } else { + startBtn.classList.add("participant-not-started"); + startBtn.title = `Participant ${index} (not start node)`; + } + + startBtn.addEventListener("click", () => { + handleStartButtonClick(startBtn, node, index); + }); + + return startBtn; + } + + function showParticipantDetails(node, index) { + const modalTitle = document.getElementById("participant-modal-title"); + const modalContent = document.getElementById("participant-modal-content"); + + modalTitle.innerHTML = `Participant ${index}`; + modalContent.innerHTML = ""; + + // Add additional info + modalContent.innerHTML += `Neighbors: ${node.neighbors.length}
Role: ${node.role}
Start: ${node.start}`; + + // Show modal + $('#participant-modal').modal('show'); + } + + function handleStartButtonClick(startBtn, node, index) { + const currentStarted = document.querySelector(".participant-started"); + if (currentStarted) { + const graph = window.TopologyManager.getGraph(); + if (!graph) return; + + const nodes = graph.graphData().nodes; + const currentStartedNode = nodes[currentStarted.getAttribute("data-id")]; + if (currentStartedNode) { + currentStartedNode.start = false; + currentStarted.classList.remove("participant-started"); + currentStarted.classList.add("participant-not-started"); + currentStarted.title = `Participant ${currentStarted.getAttribute("data-id")} (not start node)`; + } + } + + startBtn.classList.remove("participant-not-started"); + startBtn.classList.add("participant-started"); + startBtn.title = `Participant ${index} (start node)`; + node.start = true; + + // Update graph data + const graph = window.TopologyManager.getGraph(); + if (graph) { + graph.graphData(graph.graphData()); + } + } + + function setupParticipantModal() { + const modal = document.getElementById('participant-modal'); + const closeButton = modal.querySelector('.close'); + + closeButton.addEventListener('click', () => { + $(modal).modal('hide'); + }); + } + + function setupConfigButtons() { + // Save configuration button + const saveConfigBtn = document.getElementById('save-config-btn'); + if (saveConfigBtn) { + saveConfigBtn.addEventListener('click', function() { + const scenarioData = window.ScenarioManager.collectScenarioData(); + const blob = new Blob([JSON.stringify(scenarioData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${scenarioData.title || 'scenario'}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }); + } + + // Load configuration button + const loadConfigBtn = document.getElementById('load-config-btn'); + if (loadConfigBtn) { + loadConfigBtn.addEventListener('click', function() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = function(e) { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = function(e) { + try { + const scenarioData = JSON.parse(e.target.result); + window.ScenarioManager.loadScenarioData(scenarioData); + window.ScenarioManager.saveScenario(); + window.ScenarioManager.updateScenariosPosition(); + updateButtonVisibility(); + } catch (error) { + console.error('Error loading configuration:', error); + alert('Error loading configuration file. Please make sure it is a valid JSON file.'); + } + }; + reader.readAsText(file); + } + }; + input.click(); + }); + } + } + + return { + initializeUIControls, + resetModeBtn, + updateButtonVisibility, + updateParticipantDisplay, + setupParticipantModal, + handleDeployment + }; +})(); + +export default UIControls; \ No newline at end of file diff --git a/nebula/frontend/static/js/deployment/utils.js b/nebula/frontend/static/js/deployment/utils.js new file mode 100644 index 000000000..55e618494 --- /dev/null +++ b/nebula/frontend/static/js/deployment/utils.js @@ -0,0 +1,63 @@ +// Utility Functions Module +const Utils = (function() { + function showAlert(type, message) { + // Implementation of alert display + console.log(`${type}: ${message}`); + } + + function greaterThan0(input) { + const value = parseInt(input.value); + if(value < 1 && !isNaN(value)) { + input.value = 1; + } + } + + function isInRange(input, min, max) { + const value = parseFloat(input.value); + if(isNaN(value)) { + input.value = min; + } else { + input.value = Math.min(Math.max(value, min), max); + } + } + + function handleProbabilityChange(input) { + let value = parseFloat(input.value); + if (isNaN(value)) { + value = 0.5; + } else { + value = Math.min(Math.max(value, 0), 1); + } + input.value = value.toFixed(1); + + // Trigger topology update if Random is selected + const topologySelect = document.getElementById('predefined-topology-select'); + if (topologySelect && topologySelect.value === 'Random') { + window.TopologyManager.generatePredefinedTopology(); + } + } + + function atLeastOneChecked(checkboxIds) { + return checkboxIds.some(function(id) { + const checkbox = document.getElementById(id); + return checkbox && checkbox.checked; + }); + } + + function selectALL(checkboxIds, checked) { + for (let i = 0; i < checkboxIds.length; i++) { + document.getElementById(checkboxIds[i]).checked = checked; + } + } + + return { + showAlert, + greaterThan0, + isInRange, + atLeastOneChecked, + selectALL, + handleProbabilityChange + }; +})(); + +export default Utils; \ No newline at end of file diff --git a/nebula/frontend/static/js/monitor/monitor.js b/nebula/frontend/static/js/monitor/monitor.js new file mode 100644 index 000000000..0da34f016 --- /dev/null +++ b/nebula/frontend/static/js/monitor/monitor.js @@ -0,0 +1,1289 @@ +// Monitor page functionality +class Monitor { + constructor() { + // Get scenario name from URL path + const pathParts = window.location.pathname.split('/'); + this.scenarioName = pathParts[pathParts.indexOf('dashboard') + 1]; + this.offlineNodes = new Set(); + this.droneMarkers = {}; + this.droneLines = {}; + this.updateQueue = []; + this.gData = { + nodes: [], + links: [] + }; + this.nodeTimestamps = new Map(); // Track last update time for each node + + this.initializeMap(); + this.initializeGraph(); + this.initializeWebSocket(); + this.initializeEventListeners(); + this.initializeDownloadHandlers(); + this.loadInitialData(); + + this.startStaleNodeCheck(); + } + + initializeMap() { + console.log('Initializing map...'); + this.map = L.map('map', { + center: [38.023522, -1.174389], + zoom: 17, + minZoom: 2, + maxZoom: 18, + maxBounds: [[-90, -180], [90, 180]], + maxBoundsViscosity: 1.0, + zoomControl: true, + worldCopyJump: false, + }); + + console.log('Adding tile layer...'); + L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '© enriquetomasmb.com' + }).addTo(this.map); + + // Initialize line layer + console.log('Initializing line layer...'); + this.lineLayer = L.layerGroup().addTo(this.map); + console.log('Line layer added to map:', this.lineLayer); + + // Initialize drone icons + console.log('Initializing drone icons...'); + this.droneIcon = L.icon({ + iconUrl: '/platform/static/images/drone.svg', + iconSize: [28, 28], + iconAnchor: [19, 19], + popupAnchor: [0, -19] + }); + + this.droneIconOffline = L.icon({ + iconUrl: '/platform/static/images/drone_offline.svg', + iconSize: [28, 28], + iconAnchor: [19, 19], + popupAnchor: [0, -19] + }); + + // Add CSS to style the offline drone icon + const style = document.createElement('style'); + style.textContent = ` + .leaflet-marker-icon.drone-offline { + filter: brightness(0) saturate(100%) invert(15%) sepia(100%) saturate(5000%) hue-rotate(350deg) brightness(90%) contrast(100%); + } + `; + document.head.appendChild(style); + + console.log('Map initialization complete'); + } + + initializeGraph() { + const width = document.getElementById('3d-graph').offsetWidth; + + this.Graph = ForceGraph3D()(document.getElementById('3d-graph')) + .width(width) + .height(600) + .backgroundColor('#ffffff') + .nodeId('ipport') + .nodeLabel(node => this.createNodeLabel(node)) + .onNodeClick(node => this.handleNodeClick(node)) + .nodeThreeObject(node => this.createNodeObject(node)) + .linkSource('source') + .linkTarget('target') + .linkColor(link => { + const sourceNode = this.gData.nodes.find(n => n.ipport === link.source); + const targetNode = this.gData.nodes.find(n => n.ipport === link.target); + return (sourceNode && this.offlineNodes.has(sourceNode.ipport)) || + (targetNode && this.offlineNodes.has(targetNode.ipport)) ? '#ff0000' : '#999'; + }) + .linkOpacity(0.6) + .linkWidth(2) + .linkDirectionalParticles(2) + .linkDirectionalParticleSpeed(0.005) + .linkDirectionalParticleWidth(2) + .d3AlphaDecay(0.01) // Slower decay for smoother updates + .d3VelocityDecay(0.3) // Less damping for more dynamic movement + .warmupTicks(100) // Add warmup for better initial layout + .cooldownTicks(100) // Add cooldown for smoother transitions + .d3Force('center', d3.forceCenter().strength(0)) // Even stronger center force + .d3Force('charge', d3.forceManyBody().strength(0)) // Reduced repulsion + .d3Force('link', d3.forceLink().id(d => d.ipport).distance(30)); // Reduced link distance + + this.Graph.cameraPosition({ x: 0, y: 0, z: 300 }, { x: 0, y: 0, z: 0 }, 0); + + const navInfo = document.getElementsByClassName("scene-nav-info")[0]; + if (navInfo) { + navInfo.style.display = 'none'; + } + + window.addEventListener("resize", () => { + this.Graph.width(document.getElementById('3d-graph').offsetWidth); + }); + } + + layoutNodes(nodes) { + const radius = 50; // Radius of the circle + const center = { x: 0, y: 0, z: 0 }; + + nodes.forEach((node, i) => { + const angle = (i / nodes.length) * 2 * Math.PI; + // Add a small offset to ensure nodes don't overlap at center + const offset = 5; + node.x = center.x + (radius + offset) * Math.cos(angle); + node.y = center.y + (radius + offset) * Math.sin(angle); + node.z = center.z; + }); + + return nodes; + } + + loadInitialData() { + if (!this.scenarioName) { + console.error('No scenario name found in URL'); + return; + } + + console.log('Loading initial data for scenario:', this.scenarioName); + fetch(`/platform/api/dashboard/${this.scenarioName}/monitor`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + console.log('Received initial data:', data); + this.processInitialData(data); + }) + .catch(error => { + console.error('Error loading initial data:', error); + showAlert('danger', 'Error loading initial data. Please refresh the page.'); + }); + } + + processInitialData(data) { + console.log('Processing initial data:', data); + if (!data.nodes_table) { + console.warn('No nodes table in initial data'); + return; + } + + // Clear existing data + this.gData.nodes = []; + this.gData.links = []; + this.droneMarkers = {}; + this.droneLines = {}; + this.lineLayer.clearLayers(); + this.offlineNodes.clear(); // Clear offline nodes set + this.nodeTimestamps.clear(); // Clear node timestamps + + // Create a map to track unique nodes by IP:port + const uniqueNodes = new Map(); + + // First pass: create all nodes and track offline nodes + data.nodes_table.forEach(node => { + try { + console.log('Processing node:', node); + const nodeData = { + uid: node[0], + idx: node[1], + ip: node[2], + port: node[3], + role: node[4], + neighbors: node[5] || "", + latitude: parseFloat(node[6]) || 0, + longitude: parseFloat(node[7]) || 0, + timestamp: node[8], + federation: node[9], + round: node[10], + malicious: node[13], + status: node[14] + }; + + console.log('Processed node data:', nodeData); + + // Validate coordinates + if (isNaN(nodeData.latitude) || isNaN(nodeData.longitude)) { + console.warn('Invalid coordinates in initial data for node:', nodeData.uid); + // Use default coordinates if invalid + nodeData.latitude = 38.023522; + nodeData.longitude = -1.174389; + } + + // Track offline nodes + if (!nodeData.status) { + this.offlineNodes.add(nodeData.ip); + console.log('Node marked as offline during initialization:', nodeData.ip); + } + + // Set initial timestamp + const initialNodeId = `${nodeData.ip}:${nodeData.port}`; + this.nodeTimestamps.set(initialNodeId, Date.now()); + + // Update table + this.updateNode(nodeData); + + // Update map + this.updateQueue.push(nodeData); + console.log('Added node to update queue:', nodeData.uid); + + // Add node to graph data - ensure uniqueness + const uniqueNodeId = `${nodeData.ip}:${nodeData.port}`; + if (!uniqueNodes.has(uniqueNodeId)) { + uniqueNodes.set(uniqueNodeId, { + id: nodeData.idx, + ip: nodeData.ip, + port: nodeData.port, + ipport: uniqueNodeId, + role: nodeData.role, + color: this.getNodeColor({ ipport: uniqueNodeId, role: nodeData.role }) + }); + console.log('Added unique node:', uniqueNodeId); + } else { + console.log('Skipping duplicate node:', uniqueNodeId); + } + } catch (error) { + console.error('Error processing node data:', error); + } + }); + + // Convert unique nodes map to array + this.gData.nodes = Array.from(uniqueNodes.values()); + console.log('Total unique nodes:', this.gData.nodes.length); + + // Second pass: create links only between online nodes + console.log('Creating graph with', this.gData.nodes.length, 'nodes'); + for (let i = 0; i < this.gData.nodes.length; i++) { + const sourceNode = this.gData.nodes[i]; + const sourceIP = sourceNode.ip; + + // Skip if source node is offline + if (this.offlineNodes.has(sourceIP)) { + console.log('Skipping links for offline source node:', sourceIP); + continue; + } + + for (let j = i + 1; j < this.gData.nodes.length; j++) { + const targetNode = this.gData.nodes[j]; + const targetIP = targetNode.ip; + + // Skip if target node is offline + if (this.offlineNodes.has(targetIP)) { + console.log('Skipping link to offline target node:', targetIP); + continue; + } + + // Add bidirectional links only between online nodes + this.gData.links.push({ + source: sourceNode.ipport, + target: targetNode.ipport, + value: this.randomFloatFromInterval(1.0, 1.3) + }); + + this.gData.links.push({ + source: targetNode.ipport, + target: sourceNode.ipport, + value: this.randomFloatFromInterval(1.0, 1.3) + }); + } + } + + // Process queue immediately + this.processQueue(); + + // Initial graph update + this.updateGraph(); + console.log('Initial data processing complete. Total links:', this.gData.links.length); + } + + updateGraphData(data) { + const nodeId = `${data.ip}:${data.port}`; + console.log('Updating graph data for node:', nodeId); + + // Add or update node - ensure no duplication + const existingNodeIndex = this.gData.nodes.findIndex(n => n.ipport === nodeId); + if (existingNodeIndex === -1) { + // Only add if node doesn't exist + this.gData.nodes.push({ + id: data.idx, + ip: data.ip, + port: data.port, + ipport: nodeId, + role: data.role, + color: this.getNodeColor({ ipport: nodeId, role: data.role }) + }); + console.log('Added new node:', nodeId); + } else { + // Update existing node + this.gData.nodes[existingNodeIndex] = { + ...this.gData.nodes[existingNodeIndex], + role: data.role, + color: this.getNodeColor({ ipport: nodeId, role: data.role }) + }; + console.log('Updated existing node:', nodeId); + } + + // Helper function to get IP from source/target + const getIPFromLink = (linkEnd) => { + if (typeof linkEnd === 'string') { + return linkEnd.split(':')[0]; + } else if (linkEnd && typeof linkEnd === 'object') { + return linkEnd.ipport ? linkEnd.ipport.split(':')[0] : linkEnd.ip; + } + return ''; + }; + + // Normalize all existing links to use string IDs + this.gData.links = this.gData.links.map(link => ({ + source: typeof link.source === 'object' ? link.source.ipport : link.source, + target: typeof link.target === 'object' ? link.target.ipport : link.target, + value: link.value + })); + + // If node is offline, remove its links but preserve others + if (!data.status || this.offlineNodes.has(data.ip)) { + console.log('Node is offline, removing its links'); + this.gData.links = this.gData.links.filter(link => { + const sourceIP = getIPFromLink(link.source); + const targetIP = getIPFromLink(link.target); + return sourceIP !== data.ip && targetIP !== data.ip; + }); + return; + } + + // For online nodes, update their connections + if (data.neighbors) { + // Parse neighbors using consistent format + const neighbors = data.neighbors.split(/[\s,]+/).filter(ip => ip.trim() !== ''); + console.log('Processing neighbors:', neighbors); + + // Get current links for this node + const currentLinks = this.gData.links.filter(link => { + const sourceIP = getIPFromLink(link.source); + const targetIP = getIPFromLink(link.target); + return sourceIP === data.ip || targetIP === data.ip; + }); + + // Remove links to offline neighbors + this.gData.links = this.gData.links.filter(link => { + if (getIPFromLink(link.source) === data.ip || getIPFromLink(link.target) === data.ip) { + const neighborId = getIPFromLink(link.source) === data.ip ? link.target : link.source; + const neighborIP = getIPFromLink(neighborId); + return !this.offlineNodes.has(neighborIP); + } + return true; + }); + + // Add new links to online neighbors + neighbors.forEach(neighbor => { + const neighborIP = neighbor.split(':')[0]; + if (this.offlineNodes.has(neighborIP)) { + console.log('Skipping offline neighbor:', neighbor); + return; + } + + const normalizedNeighbor = neighbor.includes(':') ? neighbor : `${neighbor}:${data.port}`; + const neighborNode = this.gData.nodes.find(n => + n.ipport === normalizedNeighbor || + n.ipport.split(':')[0] === neighborIP + ); + + if (neighborNode) { + // Check if link already exists + const linkExists = this.gData.links.some(link => { + const sourceIP = getIPFromLink(link.source); + const targetIP = getIPFromLink(link.target); + return (sourceIP === data.ip && targetIP === neighborIP) || + (sourceIP === neighborIP && targetIP === data.ip); + }); + + if (!linkExists) { + console.log('Adding new link between', nodeId, 'and', normalizedNeighbor); + this.gData.links.push({ + source: nodeId, + target: normalizedNeighbor, + value: this.randomFloatFromInterval(1.0, 1.3) + }); + } + } + }); + } + + // Remove duplicate links + this.gData.links = this.gData.links.filter((link, index, self) => + index === self.findIndex((l) => { + const sourceIP1 = getIPFromLink(link.source); + const targetIP1 = getIPFromLink(link.target); + const sourceIP2 = getIPFromLink(l.source); + const targetIP2 = getIPFromLink(l.target); + return (sourceIP1 === sourceIP2 && targetIP1 === targetIP2) || + (sourceIP1 === targetIP2 && targetIP1 === sourceIP2); + }) + ); + + console.log('Final links after update:', this.gData.links); + } + + randomFloatFromInterval(min, max) { + return Math.random() * (max - min + 1) + min; + } + + createNodeLabel(node) { + return `

+ ID: ${node.id}
+ IP: ${node.ipport}
+ Role: ${node.role} +

`; + } + + handleNodeClick(node) { + const distance = 40; + const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z); + const newPos = node.x || node.y || node.z + ? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio } + : { x: 0, y: 0, z: distance }; + + this.Graph.cameraPosition(newPos, node, 3000); + } + + createNodeObject(node) { + const group = new THREE.Group(); + const nodeColor = this.getNodeColor(node); + const sphereRadius = 5; + + const material = new THREE.MeshBasicMaterial({ + color: nodeColor, + transparent: true, + opacity: 0.8, + }); + + const sphere = new THREE.Mesh( + new THREE.SphereGeometry(sphereRadius, 32, 32), + material + ); + group.add(sphere); + + const sprite = new THREE.Sprite( + new THREE.SpriteMaterial({ + map: this.createTextTexture(`NODE ${node.id}`), + depthWrite: false, + depthTest: false + }) + ); + + sprite.scale.set(10, 10 * 0.7, 5); + sprite.position.set(0, sphereRadius + 2, 0); + group.add(sprite); + + return group; + } + + getNodeColor(node) { + // Check if the node is offline using the IP + if (this.offlineNodes.has(node.ip)) { + return '#ff0000'; // Red color for offline nodes + } + + switch(node.role) { + case 'trainer': return '#7570b3'; + case 'aggregator': return '#d95f02'; + case 'server': return '#1b9e77'; + default: return '#68B0AB'; + } + } + + createTextTexture(text) { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + context.font = '40px Arial'; + context.fillStyle = 'black'; + context.fillText(text, 0, 40); + + const texture = new THREE.Texture(canvas); + texture.needsUpdate = true; + return texture; + } + + initializeWebSocket() { + if (!this.scenarioName) return; + + socket.addEventListener("message", (event) => { + try { + const data = JSON.parse(event.data); + if (data.scenario_name !== this.scenarioName) return; + + switch(data.type) { + case 'node_update': + this.handleNodeUpdate(data); + break; + case 'node_remove': + this.handleNodeRemove(data); + break; + case 'control': + console.log('Control message received:', data); + break; + default: + console.log('Unknown message type:', data.type); + } + } catch (e) { + console.error('Error parsing WebSocket message:', e); + } + }); + } + + handleNodeUpdate(data) { + try { + // Validate required fields + if (!data.uid || !data.ip) { + console.warn('Missing required fields for node update:', data); + return; + } + + // Update timestamp for this node + const nodeId = `${data.ip}:${data.port}`; + this.nodeTimestamps.set(nodeId, Date.now()); + + // Update node status + this.updateNode(data); + + // Update graph data + const existingNodeIndex = this.gData.nodes.findIndex(n => n.ipport === nodeId); + if (existingNodeIndex === -1) { + this.gData.nodes.push({ + id: data.idx, + ip: data.ip, + port: data.port, + ipport: nodeId, + role: data.role, + color: this.getNodeColor({ ipport: nodeId, role: data.role }) + }); + } else { + this.gData.nodes[existingNodeIndex] = { + ...this.gData.nodes[existingNodeIndex], + role: data.role, + color: this.getNodeColor({ ipport: nodeId, role: data.role }) + }; + } + + // Handle links + if (data.neighbors) { + const neighbors = data.neighbors.split(/[\s,]+/).filter(ip => ip.trim() !== ''); + + // Remove existing links for this node + this.gData.links = this.gData.links.filter(link => { + const sourceIP = typeof link.source === 'object' ? link.source.ipport : link.source; + const targetIP = typeof link.target === 'object' ? link.target.ipport : link.target; + return sourceIP !== nodeId && targetIP !== nodeId; + }); + + // Add new links for online neighbors + neighbors.forEach(neighbor => { + const neighborIP = neighbor.split(':')[0]; + if (!this.offlineNodes.has(neighborIP)) { + const normalizedNeighbor = neighbor.includes(':') ? neighbor : `${neighbor}:${data.port}`; + const neighborNode = this.gData.nodes.find(n => + n.ipport === normalizedNeighbor || + n.ipport.split(':')[0] === neighborIP + ); + + if (neighborNode) { + this.gData.links.push({ + source: nodeId, + target: normalizedNeighbor, + value: this.randomFloatFromInterval(1.0, 1.3) + }); + } + } + }); + } + + // Update the graph + this.updateGraph(); + } catch (error) { + console.error('Error handling node update:', error); + } + } + + hasGraphChanges(data) { + // If no data is provided, return false + if (!data) return false; + + const nodeId = `${data.ip}:${data.port}`; + const currentLinks = this.gData.links.filter(link => + link.source === nodeId || link.target === nodeId + ); + + if (!data.neighbors) return false; + + // Parse neighbors using consistent format + const neighbors = data.neighbors.split(/[\s,]+/).filter(ip => ip.trim() !== ''); + + // Create sets of current and new neighbors for comparison + const currentNeighbors = new Set( + currentLinks.map(link => { + const neighborId = link.source === nodeId ? link.target : link.source; + return neighborId.split(':')[0]; // Compare only IPs + }) + ); + + const newNeighbors = new Set( + neighbors.map(neighbor => neighbor.split(':')[0]) // Compare only IPs + ); + + // Check if there are any differences in the sets + if (currentNeighbors.size !== newNeighbors.size) return true; + + for (const neighbor of newNeighbors) { + if (!currentNeighbors.has(neighbor)) return true; + } + + return false; + } + + handleNodeRemove(data) { + try { + // Validate required fields + if (!data.uid || !data.ip) { + console.warn('Missing required fields for node removal:', data); + return; + } + + this.updateNode(data); + this.removeNodeLinks(data); + // Update graph data after removing links + this.updateGraphData(data); + this.updateGraph(); + } catch (error) { + console.error('Error handling node removal:', error); + } + } + + updateNode(data) { + const nodeRow = document.querySelector(`#node-${data.uid}`); + if (!nodeRow) return; + + const nodeId = `${data.ip}:${data.port}`; // Use full IP:port as nodeId + const wasOffline = this.offlineNodes.has(nodeId); + const isNowOffline = !data.status; + + // Update offlineNodes set based on status + if (isNowOffline) { + this.offlineNodes.add(nodeId); + console.log('Node marked as offline:', nodeId); + + // Remove all links for this node + this.removeNodeLinks(data); + + // Force immediate graph update when node goes offline + this.updateGraphData(data); + this.updateGraph(); + + // Update marker appearance + if (this.droneMarkers[data.uid]) { + this.droneMarkers[data.uid].setIcon(this.droneIconOffline); + this.droneMarkers[data.uid].getElement().classList.add('drone-offline'); + } + } else { + this.offlineNodes.delete(nodeId); + console.log('Node marked as online:', nodeId); + + // Update marker appearance + if (this.droneMarkers[data.uid]) { + this.droneMarkers[data.uid].setIcon(this.droneIcon); + this.droneMarkers[data.uid].getElement().classList.remove('drone-offline'); + } + } + + // Update all table cells + // Update IDX + const idxCell = nodeRow.querySelector('td:nth-child(1)'); + if (idxCell) { + idxCell.textContent = data.idx; + } + + // Update IP + const ipCell = nodeRow.querySelector('td:nth-child(2)'); + if (ipCell) { + ipCell.textContent = data.ip; + } + + // Update Role + const roleCell = nodeRow.querySelector('td:nth-child(3)'); + if (roleCell) { + roleCell.innerHTML = ` + + ${data.role} + + `; + } + + // Update Round + const roundCell = nodeRow.querySelector('td:nth-child(4)'); + if (roundCell) { + roundCell.textContent = data.round; + } + + // Update Behavior + const behaviorCell = nodeRow.querySelector('td:nth-child(5)'); + if (behaviorCell) { + behaviorCell.innerHTML = data.malicious === "True" + ? 'Malicious' + : 'Benign'; + } + + // Update Status + const statusCell = nodeRow.querySelector('td:nth-child(6)'); + if (statusCell) { + statusCell.innerHTML = data.status + ? 'Online' + : 'Offline'; + } + + // Update map position + this.updateQueue.push(data); + } + + removeNodeLinks(data) { + if (!data || !data.ip) { + console.warn('Invalid data provided to removeNodeLinks:', data); + return; + } + + const nodeId = `${data.ip}:${data.port}`; + console.log('Removing links for node:', nodeId); + + // Remove links from graph data + const previousLinkCount = this.gData.links.length; + + // Remove all links where this node is either source or target + this.gData.links = this.gData.links.filter(link => { + const sourceId = typeof link.source === 'object' ? link.source.ipport : link.source; + const targetId = typeof link.target === 'object' ? link.target.ipport : link.target; + return sourceId !== nodeId && targetId !== nodeId; + }); + + console.log(`Removed ${previousLinkCount - this.gData.links.length} links for node ${nodeId}`); + + // Remove lines from map + if (data.uid && this.droneLines[data.uid]) { + this.cleanupDroneLines(data.uid); + } + + // Update any related lines from other nodes + if (data.uid) { + this.updateAllRelatedLines(data.uid); + } + + // Also remove links from other nodes to this offline node + Object.entries(this.droneMarkers).forEach(([uid, marker]) => { + if (marker.neighbors) { + const neighbors = Array.isArray(marker.neighbors) + ? marker.neighbors + : (typeof marker.neighbors === 'string' + ? marker.neighbors.split(/[\s,]+/).filter(ip => ip.trim() !== '') + : []); + + // If this marker has the offline node as a neighbor, update its lines + if (neighbors.some(ip => ip.startsWith(data.ip))) { + this.updateNeighborLines(uid, marker.getLatLng(), neighbors, true); + } + } + }); + } + + updateGraph(data) { + if (data) { + // Update graph data with new node information + this.updateGraphData(data); + } + + // Helper function to get IP from source/target + const getIPFromLink = (linkEnd) => { + if (typeof linkEnd === 'string') { + return linkEnd.split(':')[0]; + } else if (linkEnd && typeof linkEnd === 'object') { + return linkEnd.ipport ? linkEnd.ipport.split(':')[0] : linkEnd.ip; + } + return ''; + }; + + // First, remove all links involving offline nodes + this.gData.links = this.gData.links.filter(link => { + const sourceIP = getIPFromLink(link.source); + const targetIP = getIPFromLink(link.target); + const sourceOffline = this.offlineNodes.has(sourceIP); + const targetOffline = this.offlineNodes.has(targetIP); + + // Remove link if either source or target is offline + if (sourceOffline || targetOffline) { + console.log(`Removing link between ${sourceIP} and ${targetIP} due to offline node`); + return false; + } + return true; + }); + + // Ensure all links have valid source and target nodes + this.gData.links = this.gData.links.filter(link => { + const sourceExists = this.gData.nodes.some(n => n.ipport === link.source); + const targetExists = this.gData.nodes.some(n => n.ipport === link.target); + return sourceExists && targetExists; + }); + + // Add links only between online nodes + this.gData.nodes.forEach(node => { + if (!this.offlineNodes.has(node.ip)) { + const nodeId = node.ipport; + // Find all online neighbors + const onlineNeighbors = this.gData.nodes.filter(n => + !this.offlineNodes.has(n.ip) && n.ipport !== nodeId + ); + + // Add links to online neighbors if they don't exist + onlineNeighbors.forEach(neighbor => { + const linkExists = this.gData.links.some(link => + (link.source === nodeId && link.target === neighbor.ipport) || + (link.source === neighbor.ipport && link.target === nodeId) + ); + + if (!linkExists) { + this.gData.links.push({ + source: nodeId, + target: neighbor.ipport, + value: this.randomFloatFromInterval(1.0, 1.3) + }); + } + }); + } + }); + + // Apply layout to nodes + const layoutedNodes = this.layoutNodes([...this.gData.nodes]); + + // Update the graph with new data + this.Graph.graphData({ + nodes: layoutedNodes, + links: this.gData.links + }); + + // Reset the force simulation to ensure proper layout + this.Graph.d3ReheatSimulation(); + + // Force a redraw + this.Graph.refresh(); + } + + initializeEventListeners() { + setInterval(() => this.processQueue(), 100); + } + + initializeDownloadHandlers() { + const downloadLinks = document.getElementsByClassName('download'); + Array.from(downloadLinks).forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + fetch(link.href) + .then(response => { + if (!response.ok) { + showAlert('danger', 'File not found'); + } else { + window.location.href = link.href; + } + }) + .catch(error => { + console.error('Error:', error); + showAlert('danger', 'Error downloading file'); + }); + }); + }); + } + + processQueue() { + while (this.updateQueue.length > 0) { + const data = this.updateQueue.shift(); + console.log('Processing queue item:', data); + this.processUpdate(data); + } + } + + processUpdate(data) { + try { + console.log('Processing update for node:', data.uid); + + // Validate required fields + if (!data.uid || !data.ip) { + console.warn('Missing required fields for node update:', data); + return; + } + + // Convert and validate coordinates + const lat = parseFloat(data.latitude); + const lng = parseFloat(data.longitude); + + console.log('Coordinates:', { lat, lng }); + + if (isNaN(lat) || isNaN(lng)) { + console.warn('Invalid coordinates for node:', data.uid, 'lat:', data.latitude, 'lng:', data.longitude); + return; + } + + // Create validated node data + const nodeData = { + ...data, + latitude: lat, + longitude: lng, + neighbors: data.neighbors || "" + }; + + console.log('Validated node data:', nodeData); + + const newLatLng = new L.LatLng(lat, lng); + + // Parse neighbors string into array, handling both space and comma separators + const neighborsIPs = nodeData.neighbors + ? nodeData.neighbors.split(/[\s,]+/).filter(ip => ip.trim() !== '') + : []; + + console.log('Parsed neighbor IPs:', neighborsIPs); + + // First update the marker + console.log('Updating drone position for node:', nodeData.uid); + this.updateDronePosition( + nodeData.uid, + nodeData.ip, + lat, + lng, + neighborsIPs, + nodeData.neighbors_distance + ); + + // Then immediately update the lines for this node + if (neighborsIPs.length > 0) { + console.log('Updating neighbor lines for node:', nodeData.uid); + this.updateNeighborLines(nodeData.uid, newLatLng, neighborsIPs, true); + } + + // Finally update any related lines from other nodes + this.updateAllRelatedLines(nodeData.uid); + } catch (error) { + console.error('Error processing update:', error); + } + } + + updateDronePosition(uid, ip, lat, lng, neighborIPs, neighborsDistance) { + console.log('Updating drone position:', { uid, ip, lat, lng }); + const droneId = uid; + const newLatLng = new L.LatLng(lat, lng); + + // Create popup content with node information + const popupContent = ` +
+
Node Information
+

IP: ${ip}

+

Location: ${Number(lat).toFixed(4)}, ${Number(lng).toFixed(4)}

+ ${neighborIPs.length > 0 ? `

Neighbors: ${neighborIPs.length}

` : ''} +
`; + + if (!this.droneMarkers[droneId]) { + console.log('Creating new marker for node:', droneId); + // Create new marker + const marker = L.marker(newLatLng, { + icon: this.offlineNodes.has(ip) ? this.droneIconOffline : this.droneIcon, + title: `Node ${uid}`, + alt: `Node ${uid}`, + className: this.offlineNodes.has(ip) ? 'drone-offline' : '' + }).addTo(this.map); + + marker.bindPopup(popupContent, { + maxWidth: 300, + className: 'drone-popup' + }); + + marker.on('mouseover', function() { + this.openPopup(); + }); + + marker.on('mouseout', function() { + this.closePopup(); + }); + + marker.ip = ip; + marker.neighbors = neighborIPs; + marker.neighbors_distance = neighborsDistance; + this.droneMarkers[droneId] = marker; + console.log('Marker created and added to map:', marker); + } else { + console.log('Updating existing marker for node:', droneId); + // Update existing marker + if (this.offlineNodes.has(ip)) { + this.droneMarkers[droneId].setIcon(this.droneIconOffline); + this.droneMarkers[droneId].getElement().classList.add('drone-offline'); + } else { + this.droneMarkers[droneId].setIcon(this.droneIcon); + this.droneMarkers[droneId].getElement().classList.remove('drone-offline'); + } + + this.droneMarkers[droneId].setLatLng(newLatLng); + this.droneMarkers[droneId].getPopup().setContent(popupContent); + this.droneMarkers[droneId].neighbors = neighborIPs; + this.droneMarkers[droneId].neighbors_distance = neighborsDistance; + console.log('Marker updated:', this.droneMarkers[droneId]); + } + } + + updateNeighborLines(droneId, droneLatLng, neighborsIPs, condition) { + console.log('Updating neighbor lines for drone:', droneId, 'with neighbors:', neighborsIPs); + console.log('Current drone position:', droneLatLng); + + // Clean up existing lines for this drone + this.cleanupDroneLines(droneId); + + if (!this.droneMarkers[droneId]) { + console.warn('No marker found for drone:', droneId); + return; + } + + // Skip if current drone is offline + if (this.offlineNodes.has(this.droneMarkers[droneId].ip)) { + console.log('Skipping line creation - current drone is offline:', this.droneMarkers[droneId].ip); + return; + } + + // Initialize droneLines array if it doesn't exist + if (!this.droneLines[droneId]) { + this.droneLines[droneId] = []; + } + + // Create new lines + neighborsIPs.forEach(neighborIP => { + // Extract IP from IP:port format if present + const neighborIPOnly = neighborIP.split(':')[0]; + const neighborMarker = this.findMarkerByIP(neighborIPOnly); + + if (neighborMarker) { + // Skip if neighbor is offline + if (this.offlineNodes.has(neighborIPOnly)) { + console.log('Skipping line creation - neighbor is offline:', neighborIPOnly); + return; + } + + console.log('Found neighbor marker for IP:', neighborIPOnly); + const neighborLatLng = neighborMarker.getLatLng(); + console.log('Neighbor position:', neighborLatLng); + + console.log('Creating line between:', droneLatLng, 'and', neighborLatLng); + + try { + // Create the line with explicit coordinates + const line = L.polyline( + [ + [droneLatLng.lat, droneLatLng.lng], + [neighborLatLng.lat, neighborLatLng.lng] + ], + { + color: '#4CAF50', + weight: 3, + opacity: 1.0, + interactive: true + } + ); + + // Add popup with distance information + try { + const distance = condition + ? (this.droneMarkers[droneId].neighbors_distance && + this.droneMarkers[droneId].neighbors_distance[neighborIP]) + : (neighborMarker.neighbors_distance && + neighborMarker.neighbors_distance[this.droneMarkers[droneId].ip]); + + line.bindPopup(` +
+

Distance: ${distance ? distance + ' m' : 'Calculating...'}

+

Status: Online

+
+ `); + } catch (err) { + console.warn('Error binding popup to line:', err); + line.bindPopup('Distance: Calculating...'); + } + + // Add hover behavior + line.on('mouseover', function() { + this.openPopup(); + }); + + // Add the line to the layer group + this.lineLayer.addLayer(line); + console.log('Line added to line layer'); + + // Store the line + this.droneLines[droneId].push(line); + console.log('Line stored in droneLines array'); + + } catch (error) { + console.error('Error creating/adding line:', error); + } + } else { + console.warn('No marker found for neighbor IP:', neighborIPOnly); + } + }); + } + + cleanupDroneLines(droneId) { + console.log('Cleaning up lines for drone:', droneId); + if (this.droneLines[droneId]) { + this.droneLines[droneId].forEach(line => { + if (line) { + try { + // Remove from layer group + this.lineLayer.removeLayer(line); + console.log('Line removed from layer'); + } catch (error) { + console.error('Error removing line:', error); + } + } + }); + } + this.droneLines[droneId] = []; + } + + updateAllRelatedLines(droneId) { + // Get the current drone's IP + const currentDroneIP = this.droneMarkers[droneId]?.ip; + if (!currentDroneIP) { + console.warn('No IP found for drone:', droneId); + return; + } + + console.log('Updating related lines for drone:', droneId, 'with IP:', currentDroneIP); + + // Update lines for all drones that have this drone as a neighbor + Object.entries(this.droneMarkers).forEach(([id, marker]) => { + if (id !== droneId && marker.neighbors) { + console.log('Processing marker:', id, 'with neighbors:', marker.neighbors, 'type:', typeof marker.neighbors); + + // Handle both string and array formats for neighbors + const neighborIPs = Array.isArray(marker.neighbors) + ? marker.neighbors + : (typeof marker.neighbors === 'string' + ? marker.neighbors.split(/[\s,]+/).filter(ip => ip.trim() !== '') + : []); + + console.log('Processed neighbor IPs:', neighborIPs); + + if (neighborIPs.some(ip => ip.startsWith(currentDroneIP))) { + console.log('Found matching neighbor, updating lines'); + this.updateNeighborLines( + id, + marker.getLatLng(), + neighborIPs, + false + ); + } + } + }); + } + + findMarkerByIP(ip) { + // Handle both IP and IP:port formats + const ipOnly = ip.split(':')[0]; + console.log('Looking for marker with IP:', ipOnly); + console.log('Available markers:', Object.values(this.droneMarkers).map(m => m.ip)); + + const marker = Object.values(this.droneMarkers).find(marker => { + const markerIP = marker.ip.split(':')[0]; + const matches = markerIP === ipOnly; + if (matches) { + console.log('Found matching marker:', markerIP); + } + return matches; + }); + + if (!marker) { + console.warn('No marker found for IP:', ipOnly); + } + return marker; + } + + startStaleNodeCheck() { + // Check for stale nodes every 5 seconds + setInterval(() => { + const currentTime = Date.now(); + const staleThreshold = 20000; // 20 seconds in milliseconds + + // Check all nodes for staleness + this.nodeTimestamps.forEach((timestamp, nodeId) => { + const timeSinceLastUpdate = currentTime - timestamp; + if (timeSinceLastUpdate > staleThreshold) { + console.log(`Node ${nodeId} is stale (${timeSinceLastUpdate}ms since last update)`); + this.markNodeAsOffline(nodeId); + } + }); + }, 5000); // Check every 5 seconds + } + + markNodeAsOffline(nodeId) { + // Find the node data from our existing data structures + const node = this.gData.nodes.find(n => n.ipport === nodeId); + if (!node) { + console.warn(`Node ${nodeId} not found in graph data`); + return; + } + + console.log(`Marking node ${nodeId} as offline`); + + // Add to offline nodes set + this.offlineNodes.add(node.ip); + + // Update node color in graph data + const nodeIndex = this.gData.nodes.findIndex(n => n.ipport === nodeId); + if (nodeIndex !== -1) { + this.gData.nodes[nodeIndex].color = '#ff0000'; // Red color for offline nodes + } + + // Remove all links involving this node + this.gData.links = this.gData.links.filter(link => { + const sourceIP = typeof link.source === 'object' ? link.source.ipport : link.source; + const targetIP = typeof link.target === 'object' ? link.target.ipport : link.target; + return sourceIP !== nodeId && targetIP !== nodeId; + }); + + // Update marker appearance if it exists + const marker = Object.entries(this.droneMarkers).find(([_, m]) => m.ip === node.ip)?.[1]; + if (marker) { + marker.setIcon(this.droneIconOffline); + marker.getElement().classList.add('drone-offline'); + + // Remove all lines connected to this node + if (this.droneLines[marker.uid]) { + this.cleanupDroneLines(marker.uid); + } + } + + // Find the node's UID from the markers + const nodeEntry = Object.entries(this.droneMarkers).find(([_, m]) => m.ip === node.ip); + if (!nodeEntry) { + console.warn(`No marker found for node ${nodeId}`); + return; + } + + const [uid, _] = nodeEntry; + + // Update table status + const nodeRow = document.querySelector(`#node-${uid}`); + if (nodeRow) { + const statusCell = nodeRow.querySelector('td:nth-child(6)'); + if (statusCell) { + statusCell.innerHTML = 'Offline'; + } + } + + // Update graph visualization + this.updateGraph(); + + // Update map visualization + this.updateAllRelatedLines(uid); + } +} + +// Initialize monitor when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + new Monitor(); +}); \ No newline at end of file diff --git a/nebula/frontend/static/js/particles.json b/nebula/frontend/static/js/particles.json new file mode 100644 index 000000000..a65537db0 --- /dev/null +++ b/nebula/frontend/static/js/particles.json @@ -0,0 +1,116 @@ +{ + "particles": { + "number": { + "value": 40, + "density": { + "enable": true, + "value_area": 800 + } + }, + "color": { + "value": "#1d2253" + }, + "shape": { + "type": "circle", + "stroke": { + "width": 0, + "color": "#000000" + }, + "polygon": { + "nb_sides": 5 + }, + "image": { + "src": "img/github.svg", + "width": 100, + "height": 100 + } + }, + "opacity": { + "value": 0.5, + "random": false, + "anim": { + "enable": false, + "speed": 1, + "opacity_min": 0.1, + "sync": false + } + }, + "size": { + "value": 5, + "random": true, + "anim": { + "enable": false, + "speed": 40, + "size_min": 0.1, + "sync": false + } + }, + "line_linked": { + "enable": true, + "distance": 150, + "color": "#1d2253", + "opacity": 0.4, + "width": 1 + }, + "move": { + "enable": true, + "speed": 2, + "direction": "none", + "random": false, + "straight": false, + "out_mode": "out", + "attract": { + "enable": false, + "rotateX": 600, + "rotateY": 1200 + } + } + }, + "interactivity": { + "detect_on": "window", + "events": { + "onhover": { + "enable": true, + "mode": "repulse" + }, + "onclick": { + "enable": true, + "mode": "push" + }, + "resize": true + }, + "modes": { + "grab": { + "distance": 400, + "line_linked": { + "opacity": 1 + } + }, + "bubble": { + "distance": 400, + "size": 40, + "duration": 2, + "opacity": 8, + "speed": 3 + }, + "repulse": { + "distance": 100 + }, + "push": { + "particles_nb": 4 + }, + "remove": { + "particles_nb": 2 + } + } + }, + "retina_detect": true, + "config_demo": { + "hide_card": false, + "background_color": "#1d2253", + "background_image": "", + "background_position": "50% 50%", + "background_repeat": "no-repeat", + "background_size": "cover" + } +} \ No newline at end of file diff --git a/nebula/frontend/static/js/particles.min.js b/nebula/frontend/static/js/particles.min.js new file mode 100644 index 000000000..b3d46d127 --- /dev/null +++ b/nebula/frontend/static/js/particles.min.js @@ -0,0 +1,9 @@ +/* ----------------------------------------------- +/* Author : Vincent Garreau - vincentgarreau.com +/* MIT license: http://opensource.org/licenses/MIT +/* Demo / Generator : vincentgarreau.com/particles.js +/* GitHub : github.com/VincentGarreau/particles.js +/* How to use? : Check the GitHub README +/* v2.0.0 +/* ----------------------------------------------- */ +function hexToRgb(e){var a=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(a,function(e,a,t,i){return a+a+t+t+i+i});var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}function clamp(e,a,t){return Math.min(Math.max(e,a),t)}function isInArray(e,a){return a.indexOf(e)>-1}var pJS=function(e,a){var t=document.querySelector("#"+e+" > .particles-js-canvas-el");this.pJS={canvas:{el:t,w:t.offsetWidth,h:t.offsetHeight},particles:{number:{value:400,density:{enable:!0,value_area:800}},color:{value:"#fff"},shape:{type:"circle",stroke:{width:0,color:"#ff0000"},polygon:{nb_sides:5},image:{src:"",width:100,height:100}},opacity:{value:1,random:!1,anim:{enable:!1,speed:2,opacity_min:0,sync:!1}},size:{value:20,random:!1,anim:{enable:!1,speed:20,size_min:0,sync:!1}},line_linked:{enable:!0,distance:100,color:"#fff",opacity:1,width:1},move:{enable:!0,speed:2,direction:"none",random:!1,straight:!1,out_mode:"out",bounce:!1,attract:{enable:!1,rotateX:3e3,rotateY:3e3}},array:[]},interactivity:{detect_on:"canvas",events:{onhover:{enable:!0,mode:"grab"},onclick:{enable:!0,mode:"push"},resize:!0},modes:{grab:{distance:100,line_linked:{opacity:1}},bubble:{distance:200,size:80,duration:.4},repulse:{distance:200,duration:.4},push:{particles_nb:4},remove:{particles_nb:2}},mouse:{}},retina_detect:!1,fn:{interact:{},modes:{},vendors:{}},tmp:{}};var i=this.pJS;a&&Object.deepExtend(i,a),i.tmp.obj={size_value:i.particles.size.value,size_anim_speed:i.particles.size.anim.speed,move_speed:i.particles.move.speed,line_linked_distance:i.particles.line_linked.distance,line_linked_width:i.particles.line_linked.width,mode_grab_distance:i.interactivity.modes.grab.distance,mode_bubble_distance:i.interactivity.modes.bubble.distance,mode_bubble_size:i.interactivity.modes.bubble.size,mode_repulse_distance:i.interactivity.modes.repulse.distance},i.fn.retinaInit=function(){i.retina_detect&&window.devicePixelRatio>1?(i.canvas.pxratio=window.devicePixelRatio,i.tmp.retina=!0):(i.canvas.pxratio=1,i.tmp.retina=!1),i.canvas.w=i.canvas.el.offsetWidth*i.canvas.pxratio,i.canvas.h=i.canvas.el.offsetHeight*i.canvas.pxratio,i.particles.size.value=i.tmp.obj.size_value*i.canvas.pxratio,i.particles.size.anim.speed=i.tmp.obj.size_anim_speed*i.canvas.pxratio,i.particles.move.speed=i.tmp.obj.move_speed*i.canvas.pxratio,i.particles.line_linked.distance=i.tmp.obj.line_linked_distance*i.canvas.pxratio,i.interactivity.modes.grab.distance=i.tmp.obj.mode_grab_distance*i.canvas.pxratio,i.interactivity.modes.bubble.distance=i.tmp.obj.mode_bubble_distance*i.canvas.pxratio,i.particles.line_linked.width=i.tmp.obj.line_linked_width*i.canvas.pxratio,i.interactivity.modes.bubble.size=i.tmp.obj.mode_bubble_size*i.canvas.pxratio,i.interactivity.modes.repulse.distance=i.tmp.obj.mode_repulse_distance*i.canvas.pxratio},i.fn.canvasInit=function(){i.canvas.ctx=i.canvas.el.getContext("2d")},i.fn.canvasSize=function(){i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i&&i.interactivity.events.resize&&window.addEventListener("resize",function(){i.canvas.w=i.canvas.el.offsetWidth,i.canvas.h=i.canvas.el.offsetHeight,i.tmp.retina&&(i.canvas.w*=i.canvas.pxratio,i.canvas.h*=i.canvas.pxratio),i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i.particles.move.enable||(i.fn.particlesEmpty(),i.fn.particlesCreate(),i.fn.particlesDraw(),i.fn.vendors.densityAutoParticles()),i.fn.vendors.densityAutoParticles()})},i.fn.canvasPaint=function(){i.canvas.ctx.fillRect(0,0,i.canvas.w,i.canvas.h)},i.fn.canvasClear=function(){i.canvas.ctx.clearRect(0,0,i.canvas.w,i.canvas.h)},i.fn.particle=function(e,a,t){if(this.radius=(i.particles.size.random?Math.random():1)*i.particles.size.value,i.particles.size.anim.enable&&(this.size_status=!1,this.vs=i.particles.size.anim.speed/100,i.particles.size.anim.sync||(this.vs=this.vs*Math.random())),this.x=t?t.x:Math.random()*i.canvas.w,this.y=t?t.y:Math.random()*i.canvas.h,this.x>i.canvas.w-2*this.radius?this.x=this.x-this.radius:this.x<2*this.radius&&(this.x=this.x+this.radius),this.y>i.canvas.h-2*this.radius?this.y=this.y-this.radius:this.y<2*this.radius&&(this.y=this.y+this.radius),i.particles.move.bounce&&i.fn.vendors.checkOverlap(this,t),this.color={},"object"==typeof e.value)if(e.value instanceof Array){var s=e.value[Math.floor(Math.random()*i.particles.color.value.length)];this.color.rgb=hexToRgb(s)}else void 0!=e.value.r&&void 0!=e.value.g&&void 0!=e.value.b&&(this.color.rgb={r:e.value.r,g:e.value.g,b:e.value.b}),void 0!=e.value.h&&void 0!=e.value.s&&void 0!=e.value.l&&(this.color.hsl={h:e.value.h,s:e.value.s,l:e.value.l});else"random"==e.value?this.color.rgb={r:Math.floor(256*Math.random())+0,g:Math.floor(256*Math.random())+0,b:Math.floor(256*Math.random())+0}:"string"==typeof e.value&&(this.color=e,this.color.rgb=hexToRgb(this.color.value));this.opacity=(i.particles.opacity.random?Math.random():1)*i.particles.opacity.value,i.particles.opacity.anim.enable&&(this.opacity_status=!1,this.vo=i.particles.opacity.anim.speed/100,i.particles.opacity.anim.sync||(this.vo=this.vo*Math.random()));var n={};switch(i.particles.move.direction){case"top":n={x:0,y:-1};break;case"top-right":n={x:.5,y:-.5};break;case"right":n={x:1,y:-0};break;case"bottom-right":n={x:.5,y:.5};break;case"bottom":n={x:0,y:1};break;case"bottom-left":n={x:-.5,y:1};break;case"left":n={x:-1,y:0};break;case"top-left":n={x:-.5,y:-.5};break;default:n={x:0,y:0}}i.particles.move.straight?(this.vx=n.x,this.vy=n.y,i.particles.move.random&&(this.vx=this.vx*Math.random(),this.vy=this.vy*Math.random())):(this.vx=n.x+Math.random()-.5,this.vy=n.y+Math.random()-.5),this.vx_i=this.vx,this.vy_i=this.vy;var r=i.particles.shape.type;if("object"==typeof r){if(r instanceof Array){var c=r[Math.floor(Math.random()*r.length)];this.shape=c}}else this.shape=r;if("image"==this.shape){var o=i.particles.shape;this.img={src:o.image.src,ratio:o.image.width/o.image.height},this.img.ratio||(this.img.ratio=1),"svg"==i.tmp.img_type&&void 0!=i.tmp.source_svg&&(i.fn.vendors.createSvgImg(this),i.tmp.pushing&&(this.img.loaded=!1))}},i.fn.particle.prototype.draw=function(){function e(){i.canvas.ctx.drawImage(r,a.x-t,a.y-t,2*t,2*t/a.img.ratio)}var a=this;if(void 0!=a.radius_bubble)var t=a.radius_bubble;else var t=a.radius;if(void 0!=a.opacity_bubble)var s=a.opacity_bubble;else var s=a.opacity;if(a.color.rgb)var n="rgba("+a.color.rgb.r+","+a.color.rgb.g+","+a.color.rgb.b+","+s+")";else var n="hsla("+a.color.hsl.h+","+a.color.hsl.s+"%,"+a.color.hsl.l+"%,"+s+")";switch(i.canvas.ctx.fillStyle=n,i.canvas.ctx.beginPath(),a.shape){case"circle":i.canvas.ctx.arc(a.x,a.y,t,0,2*Math.PI,!1);break;case"edge":i.canvas.ctx.rect(a.x-t,a.y-t,2*t,2*t);break;case"triangle":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t,a.y+t/1.66,2*t,3,2);break;case"polygon":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t/(i.particles.shape.polygon.nb_sides/3.5),a.y-t/.76,2.66*t/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,1);break;case"star":i.fn.vendors.drawShape(i.canvas.ctx,a.x-2*t/(i.particles.shape.polygon.nb_sides/4),a.y-t/1.52,2*t*2.66/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,2);break;case"image":if("svg"==i.tmp.img_type)var r=a.img.obj;else var r=i.tmp.img_obj;r&&e()}i.canvas.ctx.closePath(),i.particles.shape.stroke.width>0&&(i.canvas.ctx.strokeStyle=i.particles.shape.stroke.color,i.canvas.ctx.lineWidth=i.particles.shape.stroke.width,i.canvas.ctx.stroke()),i.canvas.ctx.fill()},i.fn.particlesCreate=function(){for(var e=0;e=i.particles.opacity.value&&(a.opacity_status=!1),a.opacity+=a.vo):(a.opacity<=i.particles.opacity.anim.opacity_min&&(a.opacity_status=!0),a.opacity-=a.vo),a.opacity<0&&(a.opacity=0)),i.particles.size.anim.enable&&(1==a.size_status?(a.radius>=i.particles.size.value&&(a.size_status=!1),a.radius+=a.vs):(a.radius<=i.particles.size.anim.size_min&&(a.size_status=!0),a.radius-=a.vs),a.radius<0&&(a.radius=0)),"bounce"==i.particles.move.out_mode)var s={x_left:a.radius,x_right:i.canvas.w,y_top:a.radius,y_bottom:i.canvas.h};else var s={x_left:-a.radius,x_right:i.canvas.w+a.radius,y_top:-a.radius,y_bottom:i.canvas.h+a.radius};switch(a.x-a.radius>i.canvas.w?(a.x=s.x_left,a.y=Math.random()*i.canvas.h):a.x+a.radius<0&&(a.x=s.x_right,a.y=Math.random()*i.canvas.h),a.y-a.radius>i.canvas.h?(a.y=s.y_top,a.x=Math.random()*i.canvas.w):a.y+a.radius<0&&(a.y=s.y_bottom,a.x=Math.random()*i.canvas.w),i.particles.move.out_mode){case"bounce":a.x+a.radius>i.canvas.w?a.vx=-a.vx:a.x-a.radius<0&&(a.vx=-a.vx),a.y+a.radius>i.canvas.h?a.vy=-a.vy:a.y-a.radius<0&&(a.vy=-a.vy)}if(isInArray("grab",i.interactivity.events.onhover.mode)&&i.fn.modes.grabParticle(a),(isInArray("bubble",i.interactivity.events.onhover.mode)||isInArray("bubble",i.interactivity.events.onclick.mode))&&i.fn.modes.bubbleParticle(a),(isInArray("repulse",i.interactivity.events.onhover.mode)||isInArray("repulse",i.interactivity.events.onclick.mode))&&i.fn.modes.repulseParticle(a),i.particles.line_linked.enable||i.particles.move.attract.enable)for(var n=e+1;n0){var c=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+c.r+","+c.g+","+c.b+","+r+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(a.x,a.y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}},i.fn.interact.attractParticles=function(e,a){var t=e.x-a.x,s=e.y-a.y,n=Math.sqrt(t*t+s*s);if(n<=i.particles.line_linked.distance){var r=t/(1e3*i.particles.move.attract.rotateX),c=s/(1e3*i.particles.move.attract.rotateY);e.vx-=r,e.vy-=c,a.vx+=r,a.vy+=c}},i.fn.interact.bounceParticles=function(e,a){var t=e.x-a.x,i=e.y-a.y,s=Math.sqrt(t*t+i*i),n=e.radius+a.radius;n>=s&&(e.vx=-e.vx,e.vy=-e.vy,a.vx=-a.vx,a.vy=-a.vy)},i.fn.modes.pushParticles=function(e,a){i.tmp.pushing=!0;for(var t=0;e>t;t++)i.particles.array.push(new i.fn.particle(i.particles.color,i.particles.opacity.value,{x:a?a.pos_x:Math.random()*i.canvas.w,y:a?a.pos_y:Math.random()*i.canvas.h})),t==e-1&&(i.particles.move.enable||i.fn.particlesDraw(),i.tmp.pushing=!1)},i.fn.modes.removeParticles=function(e){i.particles.array.splice(0,e),i.particles.move.enable||i.fn.particlesDraw()},i.fn.modes.bubbleParticle=function(e){function a(){e.opacity_bubble=e.opacity,e.radius_bubble=e.radius}function t(a,t,s,n,c){if(a!=t)if(i.tmp.bubble_duration_end){if(void 0!=s){var o=n-p*(n-a)/i.interactivity.modes.bubble.duration,l=a-o;d=a+l,"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else if(r<=i.interactivity.modes.bubble.distance){if(void 0!=s)var v=s;else var v=n;if(v!=a){var d=n-p*(n-a)/i.interactivity.modes.bubble.duration;"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else"size"==c&&(e.radius_bubble=void 0),"opacity"==c&&(e.opacity_bubble=void 0)}if(i.interactivity.events.onhover.enable&&isInArray("bubble",i.interactivity.events.onhover.mode)){var s=e.x-i.interactivity.mouse.pos_x,n=e.y-i.interactivity.mouse.pos_y,r=Math.sqrt(s*s+n*n),c=1-r/i.interactivity.modes.bubble.distance;if(r<=i.interactivity.modes.bubble.distance){if(c>=0&&"mousemove"==i.interactivity.status){if(i.interactivity.modes.bubble.size!=i.particles.size.value)if(i.interactivity.modes.bubble.size>i.particles.size.value){var o=e.radius+i.interactivity.modes.bubble.size*c;o>=0&&(e.radius_bubble=o)}else{var l=e.radius-i.interactivity.modes.bubble.size,o=e.radius-l*c;o>0?e.radius_bubble=o:e.radius_bubble=0}if(i.interactivity.modes.bubble.opacity!=i.particles.opacity.value)if(i.interactivity.modes.bubble.opacity>i.particles.opacity.value){var v=i.interactivity.modes.bubble.opacity*c;v>e.opacity&&v<=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}else{var v=e.opacity-(i.particles.opacity.value-i.interactivity.modes.bubble.opacity)*c;v=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}}}else a();"mouseleave"==i.interactivity.status&&a()}else if(i.interactivity.events.onclick.enable&&isInArray("bubble",i.interactivity.events.onclick.mode)){if(i.tmp.bubble_clicking){var s=e.x-i.interactivity.mouse.click_pos_x,n=e.y-i.interactivity.mouse.click_pos_y,r=Math.sqrt(s*s+n*n),p=((new Date).getTime()-i.interactivity.mouse.click_time)/1e3;p>i.interactivity.modes.bubble.duration&&(i.tmp.bubble_duration_end=!0),p>2*i.interactivity.modes.bubble.duration&&(i.tmp.bubble_clicking=!1,i.tmp.bubble_duration_end=!1)}i.tmp.bubble_clicking&&(t(i.interactivity.modes.bubble.size,i.particles.size.value,e.radius_bubble,e.radius,"size"),t(i.interactivity.modes.bubble.opacity,i.particles.opacity.value,e.opacity_bubble,e.opacity,"opacity"))}},i.fn.modes.repulseParticle=function(e){function a(){var a=Math.atan2(d,p);if(e.vx=u*Math.cos(a),e.vy=u*Math.sin(a),"bounce"==i.particles.move.out_mode){var t={x:e.x+e.vx,y:e.y+e.vy};t.x+e.radius>i.canvas.w?e.vx=-e.vx:t.x-e.radius<0&&(e.vx=-e.vx),t.y+e.radius>i.canvas.h?e.vy=-e.vy:t.y-e.radius<0&&(e.vy=-e.vy)}}if(i.interactivity.events.onhover.enable&&isInArray("repulse",i.interactivity.events.onhover.mode)&&"mousemove"==i.interactivity.status){var t=e.x-i.interactivity.mouse.pos_x,s=e.y-i.interactivity.mouse.pos_y,n=Math.sqrt(t*t+s*s),r={x:t/n,y:s/n},c=i.interactivity.modes.repulse.distance,o=100,l=clamp(1/c*(-1*Math.pow(n/c,2)+1)*c*o,0,50),v={x:e.x+r.x*l,y:e.y+r.y*l};"bounce"==i.particles.move.out_mode?(v.x-e.radius>0&&v.x+e.radius0&&v.y+e.radius=m&&a()}else 0==i.tmp.repulse_clicking&&(e.vx=e.vx_i,e.vy=e.vy_i)},i.fn.modes.grabParticle=function(e){if(i.interactivity.events.onhover.enable&&"mousemove"==i.interactivity.status){var a=e.x-i.interactivity.mouse.pos_x,t=e.y-i.interactivity.mouse.pos_y,s=Math.sqrt(a*a+t*t);if(s<=i.interactivity.modes.grab.distance){var n=i.interactivity.modes.grab.line_linked.opacity-s/(1/i.interactivity.modes.grab.line_linked.opacity)/i.interactivity.modes.grab.distance;if(n>0){var r=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+r.r+","+r.g+","+r.b+","+n+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(i.interactivity.mouse.pos_x,i.interactivity.mouse.pos_y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}}},i.fn.vendors.eventsListeners=function(){"window"==i.interactivity.detect_on?i.interactivity.el=window:i.interactivity.el=i.canvas.el,(i.interactivity.events.onhover.enable||i.interactivity.events.onclick.enable)&&(i.interactivity.el.addEventListener("mousemove",function(e){if(i.interactivity.el==window)var a=e.clientX,t=e.clientY;else var a=e.offsetX||e.clientX,t=e.offsetY||e.clientY;i.interactivity.mouse.pos_x=a,i.interactivity.mouse.pos_y=t,i.tmp.retina&&(i.interactivity.mouse.pos_x*=i.canvas.pxratio,i.interactivity.mouse.pos_y*=i.canvas.pxratio),i.interactivity.status="mousemove"}),i.interactivity.el.addEventListener("mouseleave",function(e){i.interactivity.mouse.pos_x=null,i.interactivity.mouse.pos_y=null,i.interactivity.status="mouseleave"})),i.interactivity.events.onclick.enable&&i.interactivity.el.addEventListener("click",function(){if(i.interactivity.mouse.click_pos_x=i.interactivity.mouse.pos_x,i.interactivity.mouse.click_pos_y=i.interactivity.mouse.pos_y,i.interactivity.mouse.click_time=(new Date).getTime(),i.interactivity.events.onclick.enable)switch(i.interactivity.events.onclick.mode){case"push":i.particles.move.enable?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):1==i.interactivity.modes.push.particles_nb?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):i.interactivity.modes.push.particles_nb>1&&i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb);break;case"remove":i.fn.modes.removeParticles(i.interactivity.modes.remove.particles_nb);break;case"bubble":i.tmp.bubble_clicking=!0;break;case"repulse":i.tmp.repulse_clicking=!0,i.tmp.repulse_count=0,i.tmp.repulse_finish=!1,setTimeout(function(){i.tmp.repulse_clicking=!1},1e3*i.interactivity.modes.repulse.duration)}})},i.fn.vendors.densityAutoParticles=function(){if(i.particles.number.density.enable){var e=i.canvas.el.width*i.canvas.el.height/1e3;i.tmp.retina&&(e/=2*i.canvas.pxratio);var a=e*i.particles.number.value/i.particles.number.density.value_area,t=i.particles.array.length-a;0>t?i.fn.modes.pushParticles(Math.abs(t)):i.fn.modes.removeParticles(t)}},i.fn.vendors.checkOverlap=function(e,a){for(var t=0;tv;v++)e.lineTo(i,0),e.translate(i,0),e.rotate(l);e.fill(),e.restore()},i.fn.vendors.exportImg=function(){window.open(i.canvas.el.toDataURL("image/png"),"_blank")},i.fn.vendors.loadImg=function(e){if(i.tmp.img_error=void 0,""!=i.particles.shape.image.src)if("svg"==e){var a=new XMLHttpRequest;a.open("GET",i.particles.shape.image.src),a.onreadystatechange=function(e){4==a.readyState&&(200==a.status?(i.tmp.source_svg=e.currentTarget.response,i.fn.vendors.checkBeforeDraw()):(console.log("Error pJS - Image not found"),i.tmp.img_error=!0))},a.send()}else{var t=new Image;t.addEventListener("load",function(){i.tmp.img_obj=t,i.fn.vendors.checkBeforeDraw()}),t.src=i.particles.shape.image.src}else console.log("Error pJS - No image.src"),i.tmp.img_error=!0},i.fn.vendors.draw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type?i.tmp.count_svg>=i.particles.number.value?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):void 0!=i.tmp.img_obj?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame))},i.fn.vendors.checkBeforeDraw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type&&void 0==i.tmp.source_svg?i.tmp.checkAnimFrame=requestAnimFrame(check):(cancelRequestAnimFrame(i.tmp.checkAnimFrame),i.tmp.img_error||(i.fn.vendors.init(),i.fn.vendors.draw())):(i.fn.vendors.init(),i.fn.vendors.draw())},i.fn.vendors.init=function(){i.fn.retinaInit(),i.fn.canvasInit(),i.fn.canvasSize(),i.fn.canvasPaint(),i.fn.particlesCreate(),i.fn.vendors.densityAutoParticles(),i.particles.line_linked.color_rgb_line=hexToRgb(i.particles.line_linked.color)},i.fn.vendors.start=function(){isInArray("image",i.particles.shape.type)?(i.tmp.img_type=i.particles.shape.image.src.substr(i.particles.shape.image.src.length-3),i.fn.vendors.loadImg(i.tmp.img_type)):i.fn.vendors.checkBeforeDraw()},i.fn.vendors.eventsListeners(),i.fn.vendors.start()};Object.deepExtend=function(e,a){for(var t in a)a[t]&&a[t].constructor&&a[t].constructor===Object?(e[t]=e[t]||{},arguments.callee(e[t],a[t])):e[t]=a[t];return e},window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}(),window.cancelRequestAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.mozCancelRequestAnimationFrame||window.oCancelRequestAnimationFrame||window.msCancelRequestAnimationFrame||clearTimeout}(),window.pJSDom=[],window.particlesJS=function(e,a){"string"!=typeof e&&(a=e,e="particles-js"),e||(e="particles-js");var t=document.getElementById(e),i="particles-js-canvas-el",s=t.getElementsByClassName(i);if(s.length)for(;s.length>0;)t.removeChild(s[0]);var n=document.createElement("canvas");n.className=i,n.style.width="100%",n.style.height="100%";var r=document.getElementById(e).appendChild(n);null!=r&&pJSDom.push(new pJS(e,a))},window.particlesJS.load=function(e,a,t){var i=new XMLHttpRequest;i.open("GET",a),i.onreadystatechange=function(a){if(4==i.readyState)if(200==i.status){var s=JSON.parse(a.currentTarget.response);window.particlesJS(e,s),t&&t()}else console.log("Error pJS - XMLHttpRequest status: "+i.status),console.log("Error pJS - File config not found")},i.send()}; \ No newline at end of file diff --git a/nebula/frontend/templates/dashboard.html b/nebula/frontend/templates/dashboard.html index 0f1eb0230..b65809a1d 100755 --- a/nebula/frontend/templates/dashboard.html +++ b/nebula/frontend/templates/dashboard.html @@ -2,18 +2,20 @@ {% block body %} {{ super() }} + +