1121 lines
57 KiB
HTML
1121 lines
57 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="{{ current_user.language if current_user.is_authenticated else 'en' }}"
|
|
dir="{{ 'rtl' if current_user.is_authenticated and current_user.language in ['ar', 'he', 'fa'] else 'ltr' }}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>
|
|
{% if script_group %}
|
|
{{ script_group.display_name }} - ScriptsManager
|
|
{% else %}
|
|
Script Group - ScriptsManager
|
|
{% endif %}
|
|
</title>
|
|
|
|
<!-- Bootstrap CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
|
|
<!-- Custom CSS -->
|
|
<link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/themes.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/responsive.css') }}" rel="stylesheet">
|
|
|
|
<!-- Socket.IO for real-time updates -->
|
|
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
|
</head>
|
|
<body data-authenticated="{{ 'true' if current_user.is_authenticated else 'false' }}"
|
|
data-user-id="{{ current_user.id if current_user.is_authenticated else '' }}"
|
|
data-current-language="{{ current_user.language if current_user.is_authenticated else 'en' }}">
|
|
|
|
<!-- Navigation -->
|
|
<nav class="navbar navbar-expand-lg" id="main-navbar">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand d-flex align-items-center" href="{{ url_for('dashboard') }}">
|
|
<img src="{{ url_for('static', filename='images/SIDEL.png') }}"
|
|
alt="SIDEL Logo"
|
|
class="sidel-logo me-2">
|
|
<span data-translate="app.title">ScriptsManager</span>
|
|
</a>
|
|
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<ul class="navbar-nav me-auto">
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{{ url_for('dashboard') }}">
|
|
<i class="bi bi-house me-1"></i>
|
|
<span data-translate="nav.dashboard">Dashboard</span>
|
|
</a>
|
|
</li>
|
|
{% if script_group %}
|
|
<li class="nav-item">
|
|
<span class="nav-link active">
|
|
<i class="bi bi-folder me-1"></i>
|
|
{{ script_group.display_name }}
|
|
</span>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
|
|
<ul class="navbar-nav">
|
|
<!-- Language Selector -->
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-translate me-1"></i>
|
|
<span id="current-language-display">{{ current_user.language.upper() if current_user.is_authenticated and current_user.language else 'EN' }}</span>
|
|
</a>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item language-option" href="#" data-lang="en">🇺🇸 English</a></li>
|
|
<li><a class="dropdown-item language-option" href="#" data-lang="es">🇪🇸 Español</a></li>
|
|
<li><a class="dropdown-item language-option" href="#" data-lang="it">🇮🇹 Italiano</a></li>
|
|
<li><a class="dropdown-item language-option" href="#" data-lang="fr">🇫🇷 Français</a></li>
|
|
</ul>
|
|
</li>
|
|
|
|
<!-- Theme Toggle -->
|
|
<li class="nav-item">
|
|
<button class="btn nav-link" id="theme-toggle" type="button">
|
|
<i class="bi bi-moon-fill" id="theme-icon"></i>
|
|
</button>
|
|
</li>
|
|
|
|
<!-- Connection Status -->
|
|
<li class="nav-item">
|
|
<span class="nav-link">
|
|
<span id="connection-status" class="connection-status disconnected" title="Connection status"></span>
|
|
</span>
|
|
</li>
|
|
|
|
<!-- User Menu -->
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-person-circle me-1"></i>
|
|
{{ current_user.username }}
|
|
</a>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item" href="#" id="user-settings">
|
|
<i class="bi bi-gear me-2"></i>
|
|
<span data-translate="nav.settings">Settings</span>
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item" href="{{ url_for('logout') }}">
|
|
<i class="bi bi-box-arrow-right me-2"></i>
|
|
<span data-translate="nav.logout">Logout</span>
|
|
</a></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<div class="container-fluid py-4">
|
|
{% if script_group %}
|
|
<!-- Script Group Header -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h1 class="h2 mb-1">{{ script_group.display_name }}</h1>
|
|
{% if script_group.get_description(user_language or 'en') %}
|
|
<p class="text-muted mb-0">{{ script_group.get_description(user_language or 'en') }}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-outline-primary" id="refresh-scripts">
|
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
|
<span data-translate="actions.refresh">Refresh</span>
|
|
</button>
|
|
{% if script_group.conda_env %}
|
|
<span class="badge bg-info ms-2">
|
|
<i class="bi bi-terminal me-1"></i>
|
|
{{ script_group.conda_env }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Project Section -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">
|
|
<i class="bi bi-folder-open me-2"></i>
|
|
Active Project
|
|
</h6>
|
|
<div>
|
|
<button class="btn btn-sm btn-outline-primary" id="change-project-btn">
|
|
<i class="bi bi-arrow-repeat"></i> Change Project
|
|
</button>
|
|
<button class="btn btn-sm btn-success" id="new-project-group-btn">
|
|
<i class="bi bi-plus"></i> New Project
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body py-2">
|
|
<div id="active-project-display">
|
|
<div class="d-flex align-items-center">
|
|
<div class="spinner-border spinner-border-sm me-2"></div>
|
|
<span>Loading project...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts Grid -->
|
|
<div class="row" id="scripts-container">
|
|
{% for script in script_group.scripts %}
|
|
<div class="col-lg-6 col-xl-4 mb-4">
|
|
<div class="card script-card h-100" data-script-id="{{ script.id }}">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0">{{ script.display_name }}</h5>
|
|
<span class="badge script-status status-{{ script.status or 'idle' }}">
|
|
{{ script.status or 'idle' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
{% if script.description %}
|
|
<p class="card-text">{{ script.description }}</p>
|
|
{% endif %}
|
|
|
|
<!-- Script Parameters Form -->
|
|
{% if script.parameters %}
|
|
<form class="script-form mb-3" data-script-id="{{ script.id }}">
|
|
{% for param in script.parameters %}
|
|
<div class="mb-2">
|
|
<label class="form-label small">{{ param.display_name or param.name }}</label>
|
|
{% if param.type == 'text' %}
|
|
<input type="text" class="form-control form-control-sm"
|
|
name="{{ param.name }}"
|
|
value="{{ param.default or '' }}"
|
|
{% if param.required %}required{% endif %}>
|
|
{% elif param.type == 'number' %}
|
|
<input type="number" class="form-control form-control-sm"
|
|
name="{{ param.name }}"
|
|
value="{{ param.default or '' }}"
|
|
{% if param.min is defined %}min="{{ param.min }}"{% endif %}
|
|
{% if param.max is defined %}max="{{ param.max }}"{% endif %}
|
|
{% if param.required %}required{% endif %}>
|
|
{% elif param.type == 'select' %}
|
|
<select class="form-select form-select-sm"
|
|
name="{{ param.name }}"
|
|
{% if param.required %}required{% endif %}>
|
|
{% for option in param.options %}
|
|
<option value="{{ option.value }}"
|
|
{% if option.value == param.default %}selected{% endif %}>
|
|
{{ option.label }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% elif param.type == 'checkbox' %}
|
|
<div class="form-check">
|
|
<input type="checkbox" class="form-check-input"
|
|
name="{{ param.name }}"
|
|
value="true"
|
|
{% if param.default %}checked{% endif %}>
|
|
<label class="form-check-label">
|
|
{{ param.description or param.display_name }}
|
|
</label>
|
|
</div>
|
|
{% endif %}
|
|
{% if param.description and param.type != 'checkbox' %}
|
|
<small class="form-text text-muted">{{ param.description }}</small>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="card-footer">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-primary btn-sm run-script"
|
|
data-script-id="{{ script.id }}">
|
|
<i class="bi bi-play-fill me-1"></i>
|
|
<span data-translate="actions.run">Run</span>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm view-logs"
|
|
data-script-id="{{ script.id }}">
|
|
<i class="bi bi-journal-text me-1"></i>
|
|
<span data-translate="actions.logs">Logs</span>
|
|
</button>
|
|
{% if can_edit %}
|
|
<button type="button" class="btn btn-outline-warning btn-sm edit-script"
|
|
data-script-id="{{ script.id }}">
|
|
<i class="bi bi-pencil me-1"></i>
|
|
<span data-translate="actions.edit">Edit</span>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if script.has_interface %}
|
|
<button type="button" class="btn btn-outline-info btn-sm open-interface"
|
|
data-script-id="{{ script.id }}">
|
|
<i class="bi bi-window me-1"></i>
|
|
<span data-translate="actions.interface">Interface</span>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% if not script_group.scripts %}
|
|
<div class="col-12">
|
|
<div class="alert alert-info text-center" role="alert">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<span data-translate="messages.no_scripts">No scripts found in this group.</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Log Modal -->
|
|
<div class="modal fade" id="logModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-journal-text me-2"></i>
|
|
<span data-translate="logs.title">Script Logs</span>
|
|
<span id="log-script-name" class="text-muted"></span>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="script-logs" class="log-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Script Modal -->
|
|
<div class="modal fade" id="editScriptModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-pencil me-2"></i>
|
|
<span data-translate="actions.edit_script">Edit Script</span>
|
|
<span id="edit-script-name" class="text-muted"></span>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editScriptForm">
|
|
<input type="hidden" id="editScriptId" name="script_id">
|
|
|
|
<div class="mb-3">
|
|
<label for="editDisplayName" class="form-label">
|
|
<span data-translate="script.display_name">Display Name</span>
|
|
</label>
|
|
<input type="text" class="form-control" id="editDisplayName"
|
|
name="display_name" required maxlength="100">
|
|
<div class="form-text">
|
|
<span data-translate="script.display_name_help">Name shown in the interface</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editDescription" class="form-label">
|
|
<span data-translate="script.description">Short Description</span>
|
|
</label>
|
|
<textarea class="form-control" id="editDescription"
|
|
name="description" rows="3" maxlength="500"></textarea>
|
|
<div class="form-text">
|
|
<span data-translate="script.description_help">Brief description shown on the card</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editDescriptionLong" class="form-label">
|
|
<span data-translate="script.description_long">Long Description (Markdown)</span>
|
|
<span class="badge bg-secondary ms-2" id="editLanguageBadge">EN</span>
|
|
</label>
|
|
<textarea class="form-control" id="editDescriptionLong"
|
|
name="description_long" rows="8"></textarea>
|
|
<div class="form-text">
|
|
<span data-translate="script.description_long_help">
|
|
Detailed description in Markdown format. Language-specific versions supported.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info" role="alert">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<span data-translate="script.markdown_info">
|
|
You can use Markdown syntax for formatting. If a description doesn't exist
|
|
in the selected language, the English version will be shown as fallback.
|
|
</span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
<span data-translate="actions.cancel">Cancel</span>
|
|
</button>
|
|
<button type="button" class="btn btn-primary" id="saveScriptBtn">
|
|
<i class="bi bi-check-lg me-1"></i>
|
|
<span data-translate="actions.save">Save Changes</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% else %}
|
|
<!-- No Script Group Selected -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="alert alert-warning text-center" role="alert">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
<span data-translate="errors.group_not_found">Script group not found.</span>
|
|
<div class="mt-2">
|
|
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
|
|
<i class="bi bi-house me-1"></i>
|
|
<span data-translate="actions.back_to_dashboard">Back to Dashboard</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Toast Container -->
|
|
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toast-container"></div>
|
|
|
|
<!-- Bootstrap JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<!-- Custom JS -->
|
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/theme-manager.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/language-manager.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/websocket.js') }}"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize script group functionality
|
|
const scriptGroup = new ScriptGroupManager();
|
|
|
|
// Initialize log display for modal
|
|
window.scriptLogDisplay = new LogDisplay('script-logs');
|
|
|
|
// Handle script execution
|
|
document.querySelectorAll('.run-script').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const scriptId = this.dataset.scriptId;
|
|
const form = document.querySelector(`.script-form[data-script-id="${scriptId}"]`);
|
|
const parameters = {};
|
|
|
|
// Collect form parameters
|
|
if (form) {
|
|
const formData = new FormData(form);
|
|
for (const [key, value] of formData.entries()) {
|
|
parameters[key] = value;
|
|
}
|
|
}
|
|
|
|
scriptGroup.runScript(scriptId, parameters);
|
|
});
|
|
});
|
|
|
|
// Handle log viewing
|
|
document.querySelectorAll('.view-logs').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const scriptId = this.dataset.scriptId;
|
|
const scriptName = this.closest('.script-card').querySelector('.card-title').textContent;
|
|
|
|
document.getElementById('log-script-name').textContent = ` - ${scriptName}`;
|
|
window.scriptLogDisplay.clear();
|
|
|
|
// Subscribe to script logs
|
|
if (window.webSocketManager) {
|
|
window.webSocketManager.subscribeToScript(scriptId);
|
|
}
|
|
|
|
// Load existing logs
|
|
scriptGroup.loadScriptLogs(scriptId);
|
|
|
|
// Show modal
|
|
new bootstrap.Modal(document.getElementById('logModal')).show();
|
|
});
|
|
});
|
|
|
|
// Handle script editing (admin only)
|
|
document.querySelectorAll('.edit-script').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const scriptId = this.dataset.scriptId;
|
|
const scriptName = this.closest('.script-card').querySelector('.card-title').textContent;
|
|
|
|
// Show loading state
|
|
const modal = new bootstrap.Modal(document.getElementById('editScriptModal'));
|
|
document.getElementById('edit-script-name').textContent = ` - ${scriptName}`;
|
|
|
|
// Load script data
|
|
loadScriptForEditing(scriptId);
|
|
modal.show();
|
|
});
|
|
});
|
|
|
|
// Handle save script changes
|
|
document.getElementById('saveScriptBtn')?.addEventListener('click', function() {
|
|
saveScriptChanges();
|
|
});
|
|
|
|
// Handle interface opening
|
|
document.querySelectorAll('.open-interface').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const scriptId = this.dataset.scriptId;
|
|
scriptGroup.openScriptInterface(scriptId);
|
|
});
|
|
});
|
|
|
|
// Handle refresh
|
|
document.getElementById('refresh-scripts')?.addEventListener('click', function() {
|
|
const button = this;
|
|
const originalHTML = button.innerHTML;
|
|
|
|
button.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> Refreshing...';
|
|
button.disabled = true;
|
|
|
|
// Get group ID from the current URL or page data
|
|
const groupId = {{ script_group.id }};
|
|
|
|
fetch(`/api/script-groups/${groupId}/refresh`, { method: 'POST' })
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
window.location.reload(); // Reload page to show updated scripts
|
|
} else {
|
|
alert('Error refreshing scripts: ' + (data.error || 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error refreshing scripts:', error);
|
|
alert('Error refreshing scripts: ' + error.message);
|
|
})
|
|
.finally(() => {
|
|
button.innerHTML = originalHTML;
|
|
button.disabled = false;
|
|
});
|
|
});
|
|
});
|
|
|
|
// Project Management Functions for Script Group Page
|
|
function loadActiveProjectForGroup() {
|
|
const groupId = {{ script_group.id }};
|
|
|
|
fetch('/api/projects/active')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const projectDisplay = document.getElementById('active-project-display');
|
|
|
|
if (data.active_project && data.active_project.group_id === groupId) {
|
|
const project = data.active_project;
|
|
projectDisplay.innerHTML = `
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>${project.project_name}</strong>
|
|
${project.description ? `<br><small class="text-muted">${project.description}</small>` : ''}
|
|
</div>
|
|
<span class="badge bg-success">Active</span>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// Load available projects for this group
|
|
fetch(`/api/projects?group_id=${groupId}`)
|
|
.then(response => response.json())
|
|
.then(projects => {
|
|
if (projects.length > 0) {
|
|
const defaultProject = projects.find(p => p.is_default) || projects[0];
|
|
setActiveProject(defaultProject.id, false);
|
|
} else {
|
|
projectDisplay.innerHTML = `
|
|
<div class="text-muted">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
No projects found for this group. Create a new project to get started.
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading active project:', error);
|
|
document.getElementById('active-project-display').innerHTML = `
|
|
<div class="text-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
Error loading project information
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function showChangeProjectModal() {
|
|
const groupId = {{ script_group.id }};
|
|
|
|
const modalHtml = `
|
|
<div class="modal fade" id="changeProjectModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Select Project</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="group-projects-list">
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status"></div>
|
|
<p class="mt-2">Loading projects...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const existingModal = document.getElementById('changeProjectModal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('changeProjectModal'));
|
|
modal.show();
|
|
|
|
// Load projects for this group
|
|
fetch(`/api/projects?group_id=${groupId}`)
|
|
.then(response => response.json())
|
|
.then(projects => {
|
|
const projectsList = document.getElementById('group-projects-list');
|
|
|
|
if (projects.length === 0) {
|
|
projectsList.innerHTML = `
|
|
<div class="text-center text-muted">
|
|
<i class="bi bi-folder-x display-4"></i>
|
|
<p class="mt-2">No projects found for this script group</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const projectsHtml = projects.map(project => `
|
|
<div class="card mb-2">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="mb-1">${project.project_name}</h6>
|
|
<small class="text-muted">${project.description || 'No description'}</small>
|
|
${project.is_default ? '<br><span class="badge bg-secondary">Default</span>' : ''}
|
|
</div>
|
|
<button class="btn btn-sm btn-primary"
|
|
onclick="setActiveProject(${project.id}, true)">
|
|
Select
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
projectsList.innerHTML = projectsHtml;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading projects:', error);
|
|
document.getElementById('group-projects-list').innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
Error loading projects
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function setActiveProject(projectId, closeModal = true) {
|
|
fetch(`/api/projects/${projectId}/set-active`, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => response.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
if (closeModal) {
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('changeProjectModal'));
|
|
if (modal) modal.hide();
|
|
}
|
|
loadActiveProjectForGroup();
|
|
ScriptsManager.showNotification('Project activated successfully!', 'success');
|
|
} else {
|
|
ScriptsManager.showNotification('Error activating project: ' + (result.error || 'Unknown error'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error setting active project:', error);
|
|
ScriptsManager.showNotification('Error activating project: ' + error.message, 'error');
|
|
});
|
|
}
|
|
|
|
function showNewProjectForGroupModal() {
|
|
const groupId = {{ script_group.id }};
|
|
const groupName = "{{ script_group.name }}";
|
|
|
|
const modalHtml = `
|
|
<div class="modal fade" id="newProjectGroupModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Create New Project for ${groupName}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="newProjectGroupForm">
|
|
<div class="mb-3">
|
|
<label for="projectNameGroup" class="form-label">Project Name</label>
|
|
<input type="text" class="form-control" id="projectNameGroup" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="projectDescriptionGroup" class="form-label">Description (Optional)</label>
|
|
<textarea class="form-control" id="projectDescriptionGroup" rows="3"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="createProjectForGroup()">Create Project</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const existingModal = document.getElementById('newProjectGroupModal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('newProjectGroupModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function createProjectForGroup() {
|
|
const groupId = {{ script_group.id }};
|
|
const form = document.getElementById('newProjectGroupForm');
|
|
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
const projectData = {
|
|
project_name: document.getElementById('projectNameGroup').value,
|
|
group_id: groupId,
|
|
description: document.getElementById('projectDescriptionGroup').value
|
|
};
|
|
|
|
fetch('/api/projects', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(projectData)
|
|
})
|
|
.then(response => response.json())
|
|
.then(result => {
|
|
if (result.id) {
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('newProjectGroupModal'));
|
|
modal.hide();
|
|
|
|
// Set the new project as active
|
|
setActiveProject(result.id, false);
|
|
ScriptsManager.showNotification('Project created and activated successfully!', 'success');
|
|
} else {
|
|
ScriptsManager.showNotification('Error creating project: ' + (result.error || 'Unknown error'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error creating project:', error);
|
|
ScriptsManager.showNotification('Error creating project: ' + error.message, 'error');
|
|
});
|
|
}
|
|
|
|
// Initialize project functionality when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadActiveProjectForGroup();
|
|
|
|
// Event listeners for project management
|
|
document.getElementById('change-project-btn')?.addEventListener('click', showChangeProjectModal);
|
|
document.getElementById('new-project-group-btn')?.addEventListener('click', showNewProjectForGroupModal);
|
|
});
|
|
|
|
// Basic ScriptsManager object for compatibility
|
|
window.ScriptsManager = {
|
|
showNotification: function(message, type = 'info') {
|
|
// Create a simple notification system
|
|
const alertClass = type === 'success' ? 'alert-success' :
|
|
type === 'error' ? 'alert-danger' :
|
|
type === 'warning' ? 'alert-warning' : 'alert-info';
|
|
|
|
const notification = document.createElement('div');
|
|
notification.className = `alert ${alertClass} alert-dismissible fade show position-fixed`;
|
|
notification.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
|
|
notification.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// Auto remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
};
|
|
|
|
// Script Group Manager Class
|
|
class ScriptGroupManager {
|
|
async runScript(scriptId, parameters = {}) {
|
|
try {
|
|
const response = await fetch(`/api/scripts/${scriptId}/execute`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
parameters: parameters
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
ScriptsManager.showNotification('Script started successfully', 'success');
|
|
this.updateScriptStatus(scriptId, 'running');
|
|
|
|
// If the script has an interface URL, wait for it to be ready before opening
|
|
if (result.interface_url) {
|
|
this.waitForScriptAndOpen(result.interface_url, scriptId);
|
|
}
|
|
} else {
|
|
ScriptsManager.showNotification(result.message || 'Failed to start script', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error running script:', error);
|
|
ScriptsManager.showNotification('Error running script', 'error');
|
|
}
|
|
}
|
|
|
|
async waitForScriptAndOpen(url, scriptId) {
|
|
const maxAttempts = 30; // Maximum 30 attempts (30 seconds)
|
|
const checkInterval = 1000; // Check every 1 second
|
|
let attempts = 0;
|
|
let cancelled = false;
|
|
|
|
// Show loading notification with open now button
|
|
const loadingNotification = this.showAdvancedNotification(
|
|
'Waiting for script interface to start...',
|
|
'info',
|
|
[{
|
|
text: 'Open Now',
|
|
action: () => {
|
|
cancelled = true;
|
|
this.hideNotification(loadingNotification);
|
|
window.open(url, '_blank');
|
|
}
|
|
}]
|
|
);
|
|
|
|
const checkServer = async () => {
|
|
if (cancelled) return;
|
|
|
|
attempts++;
|
|
|
|
try {
|
|
// Use our backend endpoint to check script availability
|
|
const response = await fetch('/api/scripts/check-interface', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ url: url })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.ready) {
|
|
// Server is ready!
|
|
this.hideNotification(loadingNotification);
|
|
ScriptsManager.showNotification('Script interface ready!', 'success');
|
|
window.open(url, '_blank');
|
|
return;
|
|
}
|
|
|
|
// Server not ready yet
|
|
if (attempts >= maxAttempts) {
|
|
this.hideNotification(loadingNotification);
|
|
ScriptsManager.showNotification(
|
|
'Script may take longer to start. You can try opening it manually.',
|
|
'warning'
|
|
);
|
|
// Still try to open, maybe it's ready now
|
|
window.open(url, '_blank');
|
|
return;
|
|
}
|
|
|
|
// Update notification with progress
|
|
this.updateNotificationText(
|
|
loadingNotification,
|
|
`Waiting for script interface... (${attempts}/${maxAttempts})`
|
|
);
|
|
|
|
// Wait and try again
|
|
setTimeout(checkServer, checkInterval);
|
|
|
|
} catch (error) {
|
|
console.error('Error checking script interface:', error);
|
|
|
|
if (attempts >= maxAttempts) {
|
|
this.hideNotification(loadingNotification);
|
|
ScriptsManager.showNotification(
|
|
'Error checking script status. Opening anyway...',
|
|
'warning'
|
|
);
|
|
window.open(url, '_blank');
|
|
return;
|
|
}
|
|
|
|
// Wait and try again
|
|
setTimeout(checkServer, checkInterval);
|
|
}
|
|
};
|
|
|
|
// Start checking
|
|
setTimeout(checkServer, 2000); // Wait 2 seconds before first check
|
|
}
|
|
|
|
showAdvancedNotification(message, type = 'info', buttons = []) {
|
|
const alertId = 'alert-' + Date.now();
|
|
let buttonsHTML = '';
|
|
|
|
if (buttons.length > 0) {
|
|
buttonsHTML = '<div class="mt-2">';
|
|
buttons.forEach((button, index) => {
|
|
buttonsHTML += `<button type="button" class="btn btn-sm btn-outline-${type} me-2" data-button-index="${index}">${button.text}</button>`;
|
|
});
|
|
buttonsHTML += '</div>';
|
|
}
|
|
|
|
const alertHTML = `
|
|
<div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">
|
|
<span class="alert-text">${message}</span>
|
|
${buttonsHTML}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
`;
|
|
|
|
const container = document.querySelector('.container-fluid') || document.body;
|
|
container.insertAdjacentHTML('afterbegin', alertHTML);
|
|
|
|
const alert = document.getElementById(alertId);
|
|
|
|
// Attach button event listeners
|
|
buttons.forEach((button, index) => {
|
|
const btn = alert.querySelector(`[data-button-index="${index}"]`);
|
|
if (btn) {
|
|
btn.addEventListener('click', button.action);
|
|
}
|
|
});
|
|
|
|
return alert;
|
|
}
|
|
|
|
hideNotification(notification) {
|
|
if (notification && notification.parentElement) {
|
|
const bsAlert = new bootstrap.Alert(notification);
|
|
bsAlert.close();
|
|
}
|
|
}
|
|
|
|
updateNotificationText(notification, text) {
|
|
if (notification) {
|
|
const textElement = notification.querySelector('.alert-text');
|
|
if (textElement) {
|
|
textElement.textContent = text;
|
|
}
|
|
}
|
|
}
|
|
|
|
async loadScriptLogs(scriptId) {
|
|
try {
|
|
const response = await fetch(`/api/scripts/${scriptId}/logs`);
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.logs) {
|
|
result.logs.forEach(log => {
|
|
window.scriptLogDisplay.appendLog(log.message, log.level, log.timestamp);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading logs:', error);
|
|
}
|
|
}
|
|
|
|
async openScriptInterface(scriptId) {
|
|
try {
|
|
const response = await fetch(`/api/scripts/${scriptId}/interface`);
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.url) {
|
|
window.open(result.url, '_blank');
|
|
} else {
|
|
ScriptsManager.showNotification(result.message || 'Interface not available', 'warning');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error opening interface:', error);
|
|
ScriptsManager.showNotification('Error opening script interface', 'error');
|
|
}
|
|
}
|
|
|
|
updateScriptStatus(scriptId, status) {
|
|
const statusElement = document.querySelector(`[data-script-id="${scriptId}"] .script-status`);
|
|
if (statusElement) {
|
|
statusElement.textContent = status;
|
|
statusElement.className = `badge script-status status-${status}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Script editing functions
|
|
async function loadScriptForEditing(scriptId) {
|
|
try {
|
|
const response = await fetch(`/api/scripts/${scriptId}`);
|
|
const script = await response.json();
|
|
|
|
if (response.ok) {
|
|
// Populate form fields
|
|
document.getElementById('editScriptId').value = script.id;
|
|
document.getElementById('editDisplayName').value = script.display_name || '';
|
|
document.getElementById('editDescription').value = script.description || '';
|
|
document.getElementById('editDescriptionLong').value = script.description_long || '';
|
|
|
|
// Update language badge
|
|
const languageBadge = document.getElementById('editLanguageBadge');
|
|
const userLanguage = script.user_language || 'en';
|
|
languageBadge.textContent = userLanguage.toUpperCase();
|
|
|
|
} else {
|
|
ScriptsManager.showNotification(script.error || 'Failed to load script data', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading script:', error);
|
|
ScriptsManager.showNotification('Error loading script data', 'error');
|
|
}
|
|
}
|
|
|
|
async function saveScriptChanges() {
|
|
const form = document.getElementById('editScriptForm');
|
|
const formData = new FormData(form);
|
|
const scriptId = formData.get('script_id');
|
|
|
|
if (!scriptId) {
|
|
ScriptsManager.showNotification('Script ID not found', 'error');
|
|
return;
|
|
}
|
|
|
|
// Get current language for long description
|
|
const languageBadge = document.getElementById('editLanguageBadge');
|
|
const currentLanguage = languageBadge.textContent.toLowerCase();
|
|
|
|
const data = {
|
|
display_name: formData.get('display_name'),
|
|
description: formData.get('description'),
|
|
description_long: formData.get('description_long'),
|
|
language: currentLanguage
|
|
};
|
|
|
|
// Show loading state
|
|
const saveBtn = document.getElementById('saveScriptBtn');
|
|
const originalText = saveBtn.innerHTML;
|
|
saveBtn.innerHTML = '<i class="bi bi-arrow-clockwise spin me-1"></i>Saving...';
|
|
saveBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch(`/api/scripts/${scriptId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
// Update the UI
|
|
updateScriptCardUI(scriptId, data);
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editScriptModal'));
|
|
modal.hide();
|
|
|
|
ScriptsManager.showNotification('Script updated successfully', 'success');
|
|
} else {
|
|
ScriptsManager.showNotification(result.error || 'Failed to update script', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving script:', error);
|
|
ScriptsManager.showNotification('Error saving script changes', 'error');
|
|
} finally {
|
|
// Restore button state
|
|
saveBtn.innerHTML = originalText;
|
|
saveBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function updateScriptCardUI(scriptId, data) {
|
|
const scriptCard = document.querySelector(`[data-script-id="${scriptId}"]`);
|
|
if (scriptCard) {
|
|
// Update display name
|
|
const titleElement = scriptCard.querySelector('.card-title');
|
|
if (titleElement && data.display_name) {
|
|
titleElement.textContent = data.display_name;
|
|
}
|
|
|
|
// Update description
|
|
const descElement = scriptCard.querySelector('.card-text');
|
|
if (descElement) {
|
|
if (data.description) {
|
|
descElement.textContent = data.description;
|
|
descElement.style.display = '';
|
|
} else {
|
|
descElement.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |