| {% extends "base.html" %} |
|
|
| {% block title %}Settings - PyRunner{% endblock %} |
|
|
| {% block content %} |
| <div class="flex"> |
| {% include "cpanel/_sidebar.html" %} |
|
|
| <div class="flex-1 p-8"> |
| <h1 class="text-2xl font-bold text-code-text mb-8">Settings</h1> |
|
|
| |
| <div class="mb-6"> |
| <div class="bg-code-surface border border-code-border rounded-xl p-1.5 inline-flex flex-wrap gap-1"> |
| <button type="button" |
| onclick="showTab('general')" |
| id="tab-general" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg bg-code-accent text-white transition-colors"> |
| General |
| </button> |
| <button type="button" |
| onclick="showTab('security')" |
| id="tab-security" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| Security |
| </button> |
| <button type="button" |
| onclick="showTab('schedules')" |
| id="tab-schedules" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| Schedules |
| </button> |
| <button type="button" |
| onclick="showTab('notifications')" |
| id="tab-notifications" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| Email |
| </button> |
| <button type="button" |
| onclick="showTab('retention')" |
| id="tab-retention" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| Retention |
| </button> |
| <button type="button" |
| onclick="showTab('workers')" |
| id="tab-workers" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| Workers |
| </button> |
| <button type="button" |
| onclick="showTab('system')" |
| id="tab-system" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| System |
| </button> |
| <button type="button" |
| onclick="showTab('backup')" |
| id="tab-backup" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| Backup |
| </button> |
| <button type="button" |
| onclick="showTab('api')" |
| id="tab-api" |
| class="tab-btn px-4 py-2 text-sm font-medium rounded-lg text-code-muted hover:text-code-text hover:bg-code-bg transition-colors"> |
| API |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="panel-general" class="tab-panel"> |
| <form method="post" action="{% url 'cpanel:general_settings' %}"> |
| {% csrf_token %} |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Instance Settings</h3> |
|
|
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ general_form.instance_name.label }}</label> |
| {{ general_form.instance_name }} |
| <p class="mt-1 text-xs text-code-muted">{{ general_form.instance_name.help_text }}</p> |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ general_form.timezone.label }}</label> |
| {{ general_form.timezone }} |
| <p class="mt-1 text-xs text-code-muted">{{ general_form.timezone.help_text }}</p> |
| </div> |
|
|
| <div class="grid grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ general_form.date_format.label }}</label> |
| {{ general_form.date_format }} |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ general_form.time_format.label }}</label> |
| {{ general_form.time_format }} |
| </div> |
| </div> |
|
|
| <div class="pt-4 border-t border-code-border"> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ general_form.admin_url_slug.label }}</label> |
| <div class="flex items-center space-x-2"> |
| <span class="text-code-muted">/</span> |
| {{ general_form.admin_url_slug }} |
| <span class="text-code-muted">/</span> |
| </div> |
| <p class="mt-1 text-xs text-code-muted">{{ general_form.admin_url_slug.help_text }}</p> |
| {% if general_form.admin_url_slug.errors %} |
| <p class="mt-1 text-xs text-code-red">{{ general_form.admin_url_slug.errors.0 }}</p> |
| {% endif %} |
| </div> |
| </div> |
| </div> |
|
|
| <button type="submit" class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors"> |
| Save Settings |
| </button> |
| </form> |
| </div> |
|
|
| |
| <div id="panel-security" class="tab-panel hidden"> |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Password</h3> |
|
|
| {% if request.user.has_usable_password %} |
| <div class="flex items-center justify-between p-4 bg-code-bg rounded-lg mb-4"> |
| <div class="flex items-center space-x-3"> |
| <div class="w-10 h-10 bg-code-green/20 rounded-full flex items-center justify-center"> |
| <svg class="w-5 h-5 text-code-green" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> |
| </svg> |
| </div> |
| <div> |
| <p class="text-code-text font-medium">Password Set</p> |
| <p class="text-sm text-code-muted">You can sign in with your password</p> |
| </div> |
| </div> |
| </div> |
| {% else %} |
| <div class="flex items-center justify-between p-4 bg-code-yellow/10 border border-code-yellow/30 rounded-lg mb-4"> |
| <div class="flex items-center space-x-3"> |
| <div class="w-10 h-10 bg-code-yellow/20 rounded-full flex items-center justify-center"> |
| <svg class="w-5 h-5 text-code-yellow" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> |
| </svg> |
| </div> |
| <div> |
| <p class="text-code-text font-medium">No Password Set</p> |
| <p class="text-sm text-code-muted">You're using magic link authentication only</p> |
| </div> |
| </div> |
| </div> |
| {% endif %} |
|
|
| <a href="{% url 'auth:change_password' %}" |
| class="inline-flex items-center px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-medium rounded-lg transition-colors"> |
| <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/> |
| </svg> |
| {% if request.user.has_usable_password %}Change Password{% else %}Set Password{% endif %} |
| </a> |
| </div> |
|
|
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Account Information</h3> |
|
|
| <div class="space-y-3"> |
| <div class="flex justify-between p-3 bg-code-bg rounded-lg"> |
| <span class="text-code-muted">Email</span> |
| <span class="text-code-text font-mono">{{ request.user.email }}</span> |
| </div> |
| <div class="flex justify-between p-3 bg-code-bg rounded-lg"> |
| <span class="text-code-muted">Account Type</span> |
| <span class="text-code-text">{% if request.user.is_superuser %}Administrator{% else %}User{% endif %}</span> |
| </div> |
| <div class="flex justify-between p-3 bg-code-bg rounded-lg"> |
| <span class="text-code-muted">Verified</span> |
| <span class="{% if request.user.is_verified %}text-code-green{% else %}text-code-yellow{% endif %}"> |
| {% if request.user.is_verified %}Yes{% else %}No{% endif %} |
| </span> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="panel-schedules" class="tab-panel hidden"> |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Schedule Control</h3> |
|
|
| <div class="flex items-center justify-between p-4 bg-code-bg rounded-lg"> |
| <div> |
| <p class="text-code-text font-medium">Global Schedule Pause</p> |
| <p class="text-sm text-code-muted"> |
| {% if settings.schedules_paused %} |
| All schedules are currently paused. |
| {% if settings.schedules_paused_at %} |
| Paused {{ settings.schedules_paused_at|timesince }} ago. |
| {% endif %} |
| {% else %} |
| All schedules are running normally. |
| {% endif %} |
| </p> |
| </div> |
|
|
| <form method="post" action="{% url 'cpanel:toggle_global_pause' %}"> |
| {% csrf_token %} |
| <button type="submit" class="px-4 py-2 rounded-lg font-medium transition-colors {% if settings.schedules_paused %}bg-code-green hover:bg-code-green/90 text-white{% else %}bg-code-yellow hover:bg-code-yellow/90 text-black{% endif %}"> |
| {% if settings.schedules_paused %}Resume All{% else %}Pause All{% endif %} |
| </button> |
| </form> |
| </div> |
|
|
| <p class="mt-4 text-xs text-code-muted"> |
| {% if settings.schedules_paused %} |
| Pausing schedules prevents any scheduled scripts from running automatically. |
| Scripts can still be run manually. |
| {% else %} |
| When paused, all scheduled scripts will stop running until resumed. |
| {% endif %} |
| </p> |
| </div> |
| </div> |
|
|
| |
| <div id="panel-notifications" class="tab-panel hidden"> |
| {% if settings.email_backend == 'disabled' %} |
| |
| <div class="bg-red-500/10 border border-red-500/30 rounded-xl p-6 max-w-2xl mb-6"> |
| <div class="flex items-start gap-3"> |
| <svg class="w-6 h-6 text-red-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/> |
| </svg> |
| <div> |
| <h3 class="text-red-400 font-semibold mb-2">Email is Disabled - Security Risk</h3> |
| <ul class="text-sm text-code-muted space-y-1"> |
| <li>Sign-in links are displayed on-screen instead of sent via email</li> |
| <li>Script failure/success notifications won't be sent</li> |
| <li>User invites must be shared manually</li> |
| </ul> |
| <p class="text-sm text-red-300 mt-3"> |
| Configure SMTP or Resend below to enable secure email delivery. |
| </p> |
| </div> |
| </div> |
| </div> |
| {% endif %} |
|
|
| <form method="post" action="{% url 'cpanel:notification_settings' %}"> |
| {% csrf_token %} |
|
|
| |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Email Backend</h3> |
|
|
| <div class="grid grid-cols-3 gap-4"> |
| {% for radio in notification_form.email_backend %} |
| <label class="relative flex items-center justify-center p-4 border border-code-border rounded-lg cursor-pointer hover:border-code-accent transition-colors {% if radio.data.selected %}border-code-accent bg-code-accent/10{% endif %}"> |
| <input type="radio" name="email_backend" value="{{ radio.data.value }}" |
| {% if radio.data.selected %}checked{% endif %} |
| class="sr-only peer" |
| onchange="toggleEmailBackend(this.value)"> |
| <span class="text-code-text font-medium">{{ radio.choice_label }}</span> |
| </label> |
| {% endfor %} |
| </div> |
| </div> |
|
|
| |
| <div id="smtp-config" class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl mb-6 {% if settings.email_backend != 'smtp' %}hidden{% endif %}"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">SMTP Configuration</h3> |
|
|
| <div class="grid grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.smtp_host.label }}</label> |
| {{ notification_form.smtp_host }} |
| {% if notification_form.smtp_host.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ notification_form.smtp_host.errors.0 }}</p> |
| {% endif %} |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.smtp_port.label }}</label> |
| {{ notification_form.smtp_port }} |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.smtp_username.label }}</label> |
| {{ notification_form.smtp_username }} |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.smtp_password.label }}</label> |
| {{ notification_form.smtp_password }} |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.smtp_from_email.label }}</label> |
| {{ notification_form.smtp_from_email }} |
| {% if notification_form.smtp_from_email.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ notification_form.smtp_from_email.errors.0 }}</p> |
| {% endif %} |
| </div> |
| <div class="flex items-center pt-8"> |
| {{ notification_form.smtp_use_tls }} |
| <label class="ml-2 text-sm text-code-text">{{ notification_form.smtp_use_tls.label }}</label> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="resend-config" class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl mb-6 {% if settings.email_backend != 'resend' %}hidden{% endif %}"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Resend Configuration</h3> |
|
|
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.resend_api_key.label }}</label> |
| {{ notification_form.resend_api_key }} |
| <p class="mt-1 text-xs text-code-muted">Leave blank to keep current API key</p> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.resend_from_email.label }}</label> |
| {{ notification_form.resend_from_email }} |
| {% if notification_form.resend_from_email.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ notification_form.resend_from_email.errors.0 }}</p> |
| {% endif %} |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Default Notification Email</h3> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ notification_form.default_notification_email.label }}</label> |
| {{ notification_form.default_notification_email }} |
| <p class="mt-1 text-xs text-code-muted">{{ notification_form.default_notification_email.help_text }}</p> |
| </div> |
| </div> |
|
|
| |
| <div class="flex items-center space-x-4"> |
| <button type="submit" class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors"> |
| Save Settings |
| </button> |
| <button type="button" |
| onclick="sendTestEmail()" |
| id="test-email-btn" |
| class="px-6 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors"> |
| Send Test Email |
| </button> |
| </div> |
| </form> |
|
|
| |
| <div id="test-email-result" class="hidden mt-4 p-4 rounded-lg max-w-2xl"></div> |
| </div> |
|
|
| |
| <div id="panel-retention" class="tab-panel hidden"> |
| <form method="post" action="{% url 'cpanel:retention_settings' %}"> |
| {% csrf_token %} |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Retention Policy</h3> |
| <p class="text-sm text-code-muted mb-4">Configure how long run logs are kept. Runs matching either condition will be deleted.</p> |
|
|
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ retention_form.retention_days.label }}</label> |
| {{ retention_form.retention_days }} |
| <p class="mt-1 text-xs text-code-muted">{{ retention_form.retention_days.help_text }}</p> |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ retention_form.retention_count.label }}</label> |
| {{ retention_form.retention_count }} |
| <p class="mt-1 text-xs text-code-muted">{{ retention_form.retention_count.help_text }}</p> |
| </div> |
|
|
| <div class="flex items-center p-4 bg-code-bg rounded-lg"> |
| {{ retention_form.auto_cleanup_enabled }} |
| <div class="ml-3"> |
| <label class="text-sm font-medium text-code-text">{{ retention_form.auto_cleanup_enabled.label }}</label> |
| <p class="text-xs text-code-muted">{{ retention_form.auto_cleanup_enabled.help_text }}</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <button type="submit" class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors"> |
| Save Settings |
| </button> |
| </form> |
|
|
| |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl mt-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Manual Cleanup</h3> |
|
|
| {% if settings.last_cleanup_at %} |
| <p class="text-sm text-code-muted mb-4">Last cleanup: {{ settings.last_cleanup_at|timesince }} ago</p> |
| {% endif %} |
|
|
| |
| <div id="cleanup-preview" class="mb-4 p-4 bg-code-bg rounded-lg"> |
| <p class="text-sm text-code-muted">Click "Preview" to see what would be deleted based on current settings.</p> |
| </div> |
|
|
| <div class="flex items-center space-x-4"> |
| <button type="button" |
| onclick="loadCleanupPreview()" |
| id="preview-btn" |
| class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors"> |
| Preview |
| </button> |
| <form method="post" action="{% url 'cpanel:manual_cleanup' %}" id="cleanup-form" class="inline"> |
| {% csrf_token %} |
| <button type="button" |
| onclick="confirmCleanup()" |
| id="cleanup-btn" |
| class="px-4 py-2 bg-code-red hover:bg-code-red/90 text-white rounded-lg transition-colors"> |
| Run Cleanup Now |
| </button> |
| </form> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="panel-workers" class="tab-panel hidden"> |
| {% if settings.worker_restart_required %} |
| |
| <div class="bg-yellow-500/10 border border-yellow-500/30 rounded-xl p-6 max-w-xl mb-6"> |
| <div class="flex items-start gap-3"> |
| <svg class="w-6 h-6 text-yellow-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> |
| </svg> |
| <div> |
| <h3 class="text-yellow-400 font-semibold mb-2">Worker Restart Required</h3> |
| <p class="text-sm text-code-muted"> |
| Settings were updated {{ settings.worker_settings_updated_at|timesince }} ago. |
| Restart the workers for changes to take effect. |
| </p> |
| <p class="text-xs text-code-muted mt-2"> |
| Run: <code class="bg-code-bg px-2 py-1 rounded">python manage.py qcluster</code> |
| </p> |
| </div> |
| </div> |
| </div> |
| {% endif %} |
|
|
| |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Worker Control</h3> |
|
|
| <div class="flex items-center justify-between p-4 bg-code-bg rounded-lg"> |
| <div> |
| <p class="text-code-text font-medium">Restart Workers</p> |
| <p class="text-sm text-code-muted"> |
| Gracefully restart task workers. Running tasks will complete before restart. |
| </p> |
| </div> |
| <button type="button" |
| onclick="restartWorkers()" |
| id="restart-workers-btn" |
| class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white rounded-lg font-medium transition-colors flex items-center gap-2"> |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> |
| </svg> |
| Restart |
| </button> |
| </div> |
|
|
| <div id="restart-result" class="hidden mt-4 p-4 rounded-lg"></div> |
| </div> |
|
|
| <form method="post" action="{% url 'cpanel:worker_settings' %}"> |
| {% csrf_token %} |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">Task Queue Configuration</h3> |
| <p class="text-sm text-code-muted mb-6"> |
| Configure Django-Q2 worker settings. Changes require a worker restart to take effect. |
| </p> |
|
|
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ worker_form.q_workers.label }}</label> |
| {{ worker_form.q_workers }} |
| <p class="mt-1 text-xs text-code-muted">{{ worker_form.q_workers.help_text }}</p> |
| {% if worker_form.q_workers.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ worker_form.q_workers.errors.0 }}</p> |
| {% endif %} |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ worker_form.q_timeout.label }}</label> |
| {{ worker_form.q_timeout }} |
| <p class="mt-1 text-xs text-code-muted">{{ worker_form.q_timeout.help_text }}</p> |
| {% if worker_form.q_timeout.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ worker_form.q_timeout.errors.0 }}</p> |
| {% endif %} |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ worker_form.q_retry.label }}</label> |
| {{ worker_form.q_retry }} |
| <p class="mt-1 text-xs text-code-muted">{{ worker_form.q_retry.help_text }}</p> |
| {% if worker_form.q_retry.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ worker_form.q_retry.errors.0 }}</p> |
| {% endif %} |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ worker_form.q_queue_limit.label }}</label> |
| {{ worker_form.q_queue_limit }} |
| <p class="mt-1 text-xs text-code-muted">{{ worker_form.q_queue_limit.help_text }}</p> |
| {% if worker_form.q_queue_limit.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ worker_form.q_queue_limit.errors.0 }}</p> |
| {% endif %} |
| </div> |
| </div> |
| </div> |
|
|
| <div class="flex items-center gap-4"> |
| <button type="submit" class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors"> |
| Save Settings |
| </button> |
| <p class="text-xs text-code-muted"> |
| Remember to restart workers after saving. |
| </p> |
| </div> |
| </form> |
| </div> |
|
|
| |
| <div id="panel-system" class="tab-panel hidden"> |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-xl mb-6"> |
| <div class="flex items-center justify-between mb-4"> |
| <h3 class="text-lg font-semibold text-code-text">System Information</h3> |
| <button type="button" |
| onclick="loadSystemInfo()" |
| id="refresh-system-btn" |
| class="px-3 py-1 text-sm bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors"> |
| Refresh |
| </button> |
| </div> |
|
|
| <div id="system-info-content" class="space-y-4"> |
| <p class="text-sm text-code-muted">Loading system information...</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="panel-backup" class="tab-panel hidden"> |
| |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">📦 Create Backup</h3> |
| <p class="text-sm text-code-muted mb-4"> |
| Export a complete backup of your PyRunner instance including scripts, secrets, settings, and execution history. |
| </p> |
|
|
| <form method="post" action="{% url 'cpanel:backup_create' %}" id="backup-create-form"> |
| {% csrf_token %} |
| <div class="space-y-4 mb-6"> |
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_create_form.backup_format.label }} |
| </label> |
| <div class="space-y-2"> |
| {% for radio in backup_create_form.backup_format %} |
| <label class="flex items-center"> |
| {{ radio.tag }} |
| <span class="ml-3 text-sm text-code-text">{{ radio.choice_label }}</span> |
| </label> |
| {% endfor %} |
| </div> |
| <p class="mt-1 text-xs text-code-muted">{{ backup_create_form.backup_format.help_text }}</p> |
| </div> |
|
|
| <div class="flex items-center"> |
| {{ backup_create_form.include_datastores }} |
| <label for="id_include_datastores" class="ml-3 text-sm text-code-text"> |
| {{ backup_create_form.include_datastores.label }} |
| </label> |
| </div> |
|
|
| <div class="flex items-center"> |
| {{ backup_create_form.include_runs }} |
| <label for="id_include_runs" class="ml-3 text-sm text-code-text"> |
| {{ backup_create_form.include_runs.label }} |
| </label> |
| </div> |
|
|
| <div class="ml-8"> |
| <label for="id_max_runs" class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_create_form.max_runs.label }} |
| </label> |
| {{ backup_create_form.max_runs }} |
| <p class="mt-1 text-xs text-code-muted">{{ backup_create_form.max_runs.help_text }}</p> |
| </div> |
|
|
| <div class="flex items-center"> |
| {{ backup_create_form.include_package_operations }} |
| <label for="id_include_package_operations" class="ml-3 text-sm text-code-text"> |
| {{ backup_create_form.include_package_operations.label }} |
| </label> |
| </div> |
| </div> |
|
|
| <div class="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg mb-4"> |
| <p class="text-sm text-yellow-300"> |
| ⚠ <strong>Important:</strong> Backup contains encrypted secrets. Keep your ENCRYPTION_KEY secure! |
| The backup can only be restored with the same encryption key. |
| </p> |
| </div> |
|
|
| <button type="submit" class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors"> |
| Download Backup |
| </button> |
| </form> |
| </div> |
|
|
| |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">📥 Restore Backup</h3> |
| <p class="text-sm text-code-muted mb-4"> |
| Upload a backup file to restore your PyRunner instance. |
| </p> |
|
|
| <form id="backup-restore-form" enctype="multipart/form-data"> |
| {% csrf_token %} |
| <div class="space-y-4 mb-6"> |
| <div> |
| <label for="id_backup_file" class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_restore_form.backup_file.label }} |
| </label> |
| <input type="file" |
| id="id_backup_file" |
| name="backup_file" |
| accept=".json,.json.gz,.gz" |
| class="block w-full text-sm text-code-text file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-code-accent file:text-white hover:file:bg-opacity-90" |
| onchange="handleBackupUpload(event)"> |
| <p class="mt-1 text-xs text-code-muted">{{ backup_restore_form.backup_file.help_text }}</p> |
| </div> |
| </div> |
|
|
| <div class="p-4 bg-red-500/10 border border-red-500/30 rounded-lg mb-4"> |
| <p class="text-sm text-red-300 mb-2"> |
| <strong>⚠ WARNING:</strong> This will delete all existing data and replace it with the backup! |
| </p> |
| <ul class="text-xs text-red-300/80 ml-4 space-y-1"> |
| <li>• ENCRYPTION_KEY must match the backup</li> |
| <li>• Current data will be backed up automatically first</li> |
| <li>• Virtual environments must be recreated after restore</li> |
| </ul> |
| </div> |
|
|
| <div id="upload-status" class="hidden mb-4"></div> |
| </form> |
| </div> |
|
|
| |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl mt-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">☁️ Scheduled S3 Backups</h3> |
| <p class="text-sm text-code-muted mb-4"> |
| Automatically backup your PyRunner instance to S3-compatible storage on a schedule. |
| </p> |
|
|
| {% if not backup_schedule_status.s3_enabled or not backup_schedule_status.s3_configured %} |
| <div class="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg mb-4"> |
| <p class="text-sm text-yellow-300"> |
| ⚠ <strong>S3 Not Configured:</strong> You must first configure and enable S3 storage in the |
| <a href="{% url 'cpanel:services' %}" class="underline hover:text-yellow-200">Services</a> page. |
| </p> |
| </div> |
| {% endif %} |
|
|
| <form method="post" action="{% url 'cpanel:backup_schedule_settings' %}" id="backup-schedule-form"> |
| {% csrf_token %} |
| <div class="space-y-4 mb-6"> |
| |
| <div class="flex items-center"> |
| {{ backup_schedule_form.s3_backup_enabled }} |
| <label for="id_s3_backup_enabled" class="ml-3 text-sm text-code-text"> |
| {{ backup_schedule_form.s3_backup_enabled.label }} |
| </label> |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_schedule_form.s3_backup_schedule.label }} |
| </label> |
| {{ backup_schedule_form.s3_backup_schedule }} |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_schedule_form.s3_backup_time.label }} |
| </label> |
| {{ backup_schedule_form.s3_backup_time }} |
| <p class="mt-1 text-xs text-code-muted">{{ backup_schedule_form.s3_backup_time.help_text }}</p> |
| </div> |
|
|
| |
| <div id="weekly-day-container"> |
| <label class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_schedule_form.s3_backup_day.label }} |
| </label> |
| {{ backup_schedule_form.s3_backup_day }} |
| <p class="mt-1 text-xs text-code-muted">{{ backup_schedule_form.s3_backup_day.help_text }}</p> |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_schedule_form.s3_backup_prefix.label }} |
| </label> |
| {{ backup_schedule_form.s3_backup_prefix }} |
| <p class="mt-1 text-xs text-code-muted">{{ backup_schedule_form.s3_backup_prefix.help_text }}</p> |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_schedule_form.s3_backup_retention_count.label }} |
| </label> |
| {{ backup_schedule_form.s3_backup_retention_count }} |
| <p class="mt-1 text-xs text-code-muted">{{ backup_schedule_form.s3_backup_retention_count.help_text }}</p> |
| </div> |
|
|
| |
| <div class="pt-2 border-t border-code-border"> |
| <p class="text-sm font-medium text-code-text mb-3">Backup Contents</p> |
|
|
| <div class="flex items-center mb-2"> |
| {{ backup_schedule_form.s3_backup_include_datastores }} |
| <label for="id_s3_backup_include_datastores" class="ml-3 text-sm text-code-text"> |
| {{ backup_schedule_form.s3_backup_include_datastores.label }} |
| </label> |
| </div> |
|
|
| <div class="flex items-center mb-2"> |
| {{ backup_schedule_form.s3_backup_include_runs }} |
| <label for="id_s3_backup_include_runs" class="ml-3 text-sm text-code-text"> |
| {{ backup_schedule_form.s3_backup_include_runs.label }} |
| </label> |
| </div> |
|
|
| <div class="ml-8"> |
| <label class="block text-sm font-medium text-code-text mb-2"> |
| {{ backup_schedule_form.s3_backup_max_runs.label }} |
| </label> |
| {{ backup_schedule_form.s3_backup_max_runs }} |
| <p class="mt-1 text-xs text-code-muted">{{ backup_schedule_form.s3_backup_max_runs.help_text }}</p> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="flex gap-3"> |
| <button type="submit" class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors" |
| {% if not backup_schedule_status.s3_configured %}disabled{% endif %}> |
| Save Schedule |
| </button> |
| <button type="button" onclick="runBackupNow()" |
| class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors" |
| {% if not backup_schedule_status.s3_configured %}disabled{% endif %}> |
| Run Backup Now |
| </button> |
| </div> |
| </form> |
|
|
| |
| {% if backup_schedule_status.last_run %} |
| <div class="mt-6 pt-4 border-t border-code-border"> |
| <h4 class="text-sm font-medium text-code-text mb-3">Last Backup Status</h4> |
| <div class="grid grid-cols-2 gap-4 text-sm"> |
| <div> |
| <span class="text-code-muted">Last Run:</span> |
| <span class="text-code-text ml-2">{{ backup_schedule_status.last_run|date:"Y-m-d H:i" }}</span> |
| </div> |
| <div> |
| <span class="text-code-muted">Status:</span> |
| {% if backup_schedule_status.last_status == "success" %} |
| <span class="ml-2 px-2 py-0.5 bg-green-500/20 text-green-400 rounded text-xs">Success</span> |
| {% elif backup_schedule_status.last_status == "failed" %} |
| <span class="ml-2 px-2 py-0.5 bg-red-500/20 text-red-400 rounded text-xs">Failed</span> |
| {% else %} |
| <span class="ml-2 text-code-muted">-</span> |
| {% endif %} |
| </div> |
| {% if backup_schedule_status.last_size %} |
| <div> |
| <span class="text-code-muted">Size:</span> |
| <span class="text-code-text ml-2">{{ backup_schedule_status.last_size|filesizeformat }}</span> |
| </div> |
| {% endif %} |
| {% if backup_schedule_status.next_run %} |
| <div> |
| <span class="text-code-muted">Next Run:</span> |
| <span class="text-code-text ml-2">{{ backup_schedule_status.next_run|date:"Y-m-d H:i" }}</span> |
| </div> |
| {% endif %} |
| </div> |
| {% if backup_schedule_status.last_status == "failed" and backup_schedule_status.last_error %} |
| <div class="mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg"> |
| <p class="text-sm text-red-300">{{ backup_schedule_status.last_error }}</p> |
| </div> |
| {% endif %} |
| </div> |
| {% endif %} |
| </div> |
| </div> |
|
|
| |
| <div id="panel-api" class="tab-panel hidden"> |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl mb-6"> |
| <h3 class="text-lg font-semibold text-code-text mb-4">API Access</h3> |
| <p class="text-sm text-code-muted mb-4"> |
| PyRunner provides a REST API for accessing datastore data programmatically. |
| Create API tokens to allow external applications to read your datastore entries. |
| </p> |
|
|
| <div class="p-4 bg-code-bg rounded-lg mb-6"> |
| <h4 class="text-sm font-medium text-code-text mb-2">API Endpoints</h4> |
| <div class="space-y-2 text-sm font-mono text-code-muted"> |
| <p>GET /api/v1/datastores/</p> |
| <p>GET /api/v1/datastores/{name}/</p> |
| <p>GET /api/v1/datastores/{name}/entries/</p> |
| <p>GET /api/v1/datastores/{name}/entries/{key}/</p> |
| </div> |
| </div> |
|
|
| <a href="{% url 'cpanel:api_token_list' %}" |
| class="inline-flex items-center px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors"> |
| <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/> |
| </svg> |
| Manage API Tokens |
| </a> |
|
|
| <div class="mt-4 pt-4 border-t border-code-border"> |
| <p class="text-sm text-code-muted mb-2"> |
| Each datastore has API documentation with curl examples on its detail page. |
| </p> |
| <a href="{% url 'cpanel:datastore_list' %}" |
| class="text-sm text-code-accent hover:underline inline-flex items-center"> |
| <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/> |
| </svg> |
| View Datastores |
| </a> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="restore-preview-modal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50" style="display: none;"> |
| <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl w-full mx-4"> |
| <div class="flex items-center justify-between mb-4"> |
| <h3 class="text-lg font-semibold text-code-text">Backup Preview</h3> |
| <button type="button" onclick="closeRestoreModal()" class="text-code-muted hover:text-code-text"> |
| <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> |
| </svg> |
| </button> |
| </div> |
|
|
| <div id="preview-content" class="mb-6"> |
| |
| </div> |
|
|
| <form method="post" action="{% url 'cpanel:backup_restore' %}" id="confirm-restore-form"> |
| {% csrf_token %} |
| <div class="space-y-4 mb-6"> |
| <div class="flex items-center"> |
| <input type="checkbox" name="restore_runs" id="id_restore_runs_modal" checked |
| class="w-5 h-5 text-code-accent bg-code-bg border-code-border rounded focus:ring-code-accent focus:ring-2"> |
| <label for="id_restore_runs_modal" class="ml-3 text-sm text-code-text"> |
| Restore run history |
| </label> |
| </div> |
|
|
| <div class="flex items-center"> |
| <input type="checkbox" name="confirm_delete" id="id_confirm_delete" required |
| class="w-5 h-5 text-red-500 bg-code-bg border-code-border rounded focus:ring-red-500 focus:ring-2"> |
| <label for="id_confirm_delete" class="ml-3 text-sm text-code-text"> |
| <strong>I understand all existing data will be deleted</strong> |
| </label> |
| </div> |
| </div> |
|
|
| <div class="flex gap-3"> |
| <button type="button" onclick="closeRestoreModal()" |
| class="flex-1 px-6 py-2 bg-code-bg border border-code-border hover:border-code-accent text-code-text font-semibold rounded-lg transition-colors"> |
| Cancel |
| </button> |
| <button type="submit" |
| class="flex-1 px-6 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg transition-colors"> |
| Confirm Restore |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| <script> |
| |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
| |
| function showTab(tabName) { |
| |
| document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden')); |
| |
| document.querySelectorAll('.tab-btn').forEach(t => { |
| t.classList.remove('bg-code-accent', 'text-white'); |
| t.classList.add('text-code-muted', 'hover:text-code-text', 'hover:bg-code-bg'); |
| }); |
| |
| document.getElementById('panel-' + tabName).classList.remove('hidden'); |
| |
| const tab = document.getElementById('tab-' + tabName); |
| tab.classList.remove('text-code-muted', 'hover:text-code-text', 'hover:bg-code-bg'); |
| tab.classList.add('bg-code-accent', 'text-white'); |
| |
| if (tabName === 'system') { |
| loadSystemInfo(); |
| } |
| } |
| |
| function toggleEmailBackend(backend) { |
| document.getElementById('smtp-config').classList.toggle('hidden', backend !== 'smtp'); |
| document.getElementById('resend-config').classList.toggle('hidden', backend !== 'resend'); |
| } |
| |
| function sendTestEmail() { |
| const btn = document.getElementById('test-email-btn'); |
| const resultDiv = document.getElementById('test-email-result'); |
| |
| btn.disabled = true; |
| btn.textContent = 'Sending...'; |
| resultDiv.classList.add('hidden'); |
| |
| fetch('{% url "cpanel:test_email" %}', { |
| method: 'POST', |
| headers: { |
| 'X-CSRFToken': '{{ csrf_token }}', |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| resultDiv.classList.remove('hidden'); |
| if (data.success) { |
| resultDiv.className = 'mt-4 p-4 rounded-lg max-w-2xl bg-code-green/10 border border-code-green text-code-green'; |
| resultDiv.textContent = data.message || 'Test email sent successfully!'; |
| } else { |
| resultDiv.className = 'mt-4 p-4 rounded-lg max-w-2xl bg-code-red/10 border border-code-red text-code-red'; |
| resultDiv.textContent = 'Error: ' + (data.error || 'Unknown error'); |
| } |
| }) |
| .catch(error => { |
| resultDiv.classList.remove('hidden'); |
| resultDiv.className = 'mt-4 p-4 rounded-lg max-w-2xl bg-code-red/10 border border-code-red text-code-red'; |
| resultDiv.textContent = 'Error: ' + error.message; |
| }) |
| .finally(() => { |
| btn.disabled = false; |
| btn.textContent = 'Send Test Email'; |
| }); |
| } |
| |
| function loadCleanupPreview() { |
| const btn = document.getElementById('preview-btn'); |
| const previewDiv = document.getElementById('cleanup-preview'); |
| |
| btn.disabled = true; |
| btn.textContent = 'Loading...'; |
| |
| fetch('{% url "cpanel:cleanup_preview" %}', { |
| method: 'GET', |
| headers: { |
| 'X-CSRFToken': '{{ csrf_token }}', |
| }, |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| const stats = data.stats; |
| let html = '<div class="space-y-2">'; |
| html += `<p class="text-sm text-code-text"><strong>Total runs:</strong> ${escapeHtml(String(stats.total_runs))}</p>`; |
| html += `<p class="text-sm text-code-text"><strong>Runs to delete:</strong> <span class="text-code-red">${escapeHtml(String(stats.runs_to_delete))}</span></p>`; |
| html += `<p class="text-sm text-code-text"><strong>Runs to keep:</strong> <span class="text-code-green">${escapeHtml(String(stats.runs_to_keep))}</span></p>`; |
| |
| if (stats.per_script && stats.per_script.length > 0) { |
| html += '<div class="mt-3 pt-3 border-t border-code-border">'; |
| html += '<p class="text-xs text-code-muted mb-2">Per script breakdown:</p>'; |
| html += '<div class="max-h-32 overflow-y-auto space-y-1">'; |
| stats.per_script.forEach(script => { |
| if (script.to_delete > 0) { |
| html += `<p class="text-xs text-code-muted">${escapeHtml(script.script_name)}: <span class="text-code-red">-${escapeHtml(String(script.to_delete))}</span> / ${escapeHtml(String(script.total_runs))}</p>`; |
| } |
| }); |
| html += '</div></div>'; |
| } |
| html += '</div>'; |
| previewDiv.innerHTML = html; |
| } else { |
| previewDiv.textContent = 'Error: ' + (data.error || 'Unknown error'); |
| previewDiv.className = 'text-sm text-code-red'; |
| } |
| }) |
| .catch(error => { |
| previewDiv.textContent = 'Error: ' + error.message; |
| previewDiv.className = 'text-sm text-code-red'; |
| }) |
| .finally(() => { |
| btn.disabled = false; |
| btn.textContent = 'Preview'; |
| }); |
| } |
| |
| function confirmCleanup() { |
| if (confirm('Are you sure you want to delete old runs? This action cannot be undone.')) { |
| document.getElementById('cleanup-form').submit(); |
| } |
| } |
| |
| function loadSystemInfo() { |
| const contentDiv = document.getElementById('system-info-content'); |
| const btn = document.getElementById('refresh-system-btn'); |
| |
| btn.disabled = true; |
| btn.textContent = 'Loading...'; |
| contentDiv.innerHTML = '<p class="text-sm text-code-muted">Loading system information...</p>'; |
| |
| fetch('{% url "cpanel:system_info" %}', { |
| method: 'GET', |
| headers: { |
| 'X-CSRFToken': '{{ csrf_token }}', |
| }, |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| const info = data.data; |
| const worker = info.worker_status; |
| |
| const statusClasses = {'running': 'text-code-green', 'stopped': 'text-code-red'}; |
| const workerStatusClass = statusClasses[worker.status] || 'text-code-yellow'; |
| |
| let html = ` |
| <div class="grid grid-cols-2 gap-4"> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">PyRunner Version</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(info.version))}</p> |
| </div> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">Python Version</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(info.python_version))}</p> |
| </div> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">Uptime</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(info.uptime))}</p> |
| </div> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">Database Size</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(info.database_size_display))}</p> |
| </div> |
| <div class="p-4 bg-code-bg rounded-lg col-span-2"> |
| <p class="text-xs text-code-muted mb-1">Environments Disk Usage</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(info.environments_size_display))}</p> |
| </div> |
| </div> |
| |
| <div class="mt-6 pt-6 border-t border-code-border"> |
| <h4 class="text-sm font-semibold text-code-text mb-3">Task Queue Status</h4> |
| <div class="grid grid-cols-2 gap-4"> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">Worker Status</p> |
| <p class="${workerStatusClass} font-semibold">${escapeHtml(String(worker.status_text))}</p> |
| </div> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">Configured Workers</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(worker.configured_workers))}</p> |
| </div> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">Queued Tasks</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(worker.queued_tasks))}</p> |
| </div> |
| <div class="p-4 bg-code-bg rounded-lg"> |
| <p class="text-xs text-code-muted mb-1">Tasks (Last Hour)</p> |
| <p class="text-code-text font-mono">${escapeHtml(String(worker.recent_tasks))}</p> |
| </div> |
| </div> |
| </div> |
| `; |
| contentDiv.innerHTML = html; |
| } else { |
| contentDiv.textContent = 'Error: ' + (data.error || 'Unknown error'); |
| contentDiv.className = 'text-sm text-code-red'; |
| } |
| }) |
| .catch(error => { |
| contentDiv.textContent = 'Error: ' + error.message; |
| contentDiv.className = 'text-sm text-code-red'; |
| }) |
| .finally(() => { |
| btn.disabled = false; |
| btn.textContent = 'Refresh'; |
| }); |
| } |
| |
| |
| function handleBackupUpload(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| const statusDiv = document.getElementById('upload-status'); |
| statusDiv.classList.remove('hidden'); |
| statusDiv.innerHTML = '<p class="text-sm text-code-muted">Validating backup file...</p>'; |
| |
| const formData = new FormData(); |
| formData.append('backup_file', file); |
| formData.append('csrfmiddlewaretoken', '{{ csrf_token }}'); |
| |
| fetch('{% url "cpanel:backup_upload" %}', { |
| method: 'POST', |
| body: formData, |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| statusDiv.innerHTML = '<p class="text-sm text-code-green">✓ Backup validated successfully!</p>'; |
| showRestorePreview(data.preview, data.warnings); |
| } else { |
| let errorHtml = '<div class="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">'; |
| errorHtml += '<p class="text-sm text-red-300 font-semibold mb-2">Validation failed:</p>'; |
| errorHtml += '<p class="text-sm text-red-300">' + escapeHtml(data.error || 'Unknown error') + '</p>'; |
| if (data.errors && data.errors.length > 0) { |
| errorHtml += '<ul class="text-xs text-red-300/80 mt-2 ml-4 space-y-1">'; |
| data.errors.forEach(err => { |
| errorHtml += '<li>• ' + escapeHtml(String(err)) + '</li>'; |
| }); |
| errorHtml += '</ul>'; |
| } |
| errorHtml += '</div>'; |
| statusDiv.innerHTML = errorHtml; |
| } |
| }) |
| .catch(error => { |
| statusDiv.innerHTML = '<div class="p-4 bg-red-500/10 border border-red-500/30 rounded-lg"><p class="text-sm text-red-300">Upload error: ' + escapeHtml(error.message) + '</p></div>'; |
| }); |
| } |
| |
| function showRestorePreview(preview, warnings) { |
| const modal = document.getElementById('restore-preview-modal'); |
| const content = document.getElementById('preview-content'); |
| |
| let html = '<div class="space-y-4">'; |
| html += '<div class="p-4 bg-code-bg rounded-lg">'; |
| html += '<p class="text-sm text-code-muted">Source Instance</p>'; |
| html += '<p class="text-code-text font-semibold">' + escapeHtml(String(preview.instance_name)) + '</p>'; |
| html += '</div>'; |
| |
| html += '<div class="p-4 bg-code-bg rounded-lg">'; |
| html += '<p class="text-sm text-code-muted">Created</p>'; |
| html += '<p class="text-code-text">' + escapeHtml(String(preview.created_at)) + '</p>'; |
| if (preview.created_by_email) { |
| html += '<p class="text-xs text-code-muted mt-1">By: ' + escapeHtml(String(preview.created_by_email)) + '</p>'; |
| } |
| html += '</div>'; |
| |
| html += '<div class="p-4 bg-code-bg rounded-lg">'; |
| html += '<p class="text-sm text-code-muted mb-2">This backup contains:</p>'; |
| html += '<div class="grid grid-cols-2 gap-2 text-sm text-code-text">'; |
| html += '<div>• Scripts: <strong>' + escapeHtml(String(preview.counts.scripts)) + '</strong></div>'; |
| html += '<div>• Environments: <strong>' + escapeHtml(String(preview.counts.environments)) + '</strong></div>'; |
| html += '<div>• Secrets: <strong>' + escapeHtml(String(preview.counts.secrets)) + '</strong></div>'; |
| html += '<div>• Runs: <strong>' + escapeHtml(String(preview.counts.runs)) + '</strong></div>'; |
| html += '<div>• Schedules: <strong>' + escapeHtml(String(preview.counts.schedules)) + '</strong></div>'; |
| html += '<div>• Users: <strong>' + escapeHtml(String(preview.counts.users)) + '</strong></div>'; |
| html += '<div>• DataStores: <strong>' + escapeHtml(String(preview.counts.datastores || 0)) + '</strong></div>'; |
| html += '<div>• DataStore Entries: <strong>' + escapeHtml(String(preview.counts.datastore_entries || 0)) + '</strong></div>'; |
| html += '</div>'; |
| html += '</div>'; |
| |
| if (warnings && warnings.length > 0) { |
| html += '<div class="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">'; |
| html += '<p class="text-sm text-yellow-300 font-semibold mb-2">⚠ Warnings:</p>'; |
| html += '<ul class="text-xs text-yellow-300/80 ml-4 space-y-1">'; |
| warnings.forEach(warning => { |
| html += '<li>• ' + escapeHtml(String(warning)) + '</li>'; |
| }); |
| html += '</ul>'; |
| html += '</div>'; |
| } |
| |
| html += '<div class="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">'; |
| html += '<p class="text-sm text-red-300 font-semibold mb-2">CRITICAL WARNINGS:</p>'; |
| html += '<ul class="text-xs text-red-300/80 ml-4 space-y-1">'; |
| html += '<li>• All existing data will be DELETED</li>'; |
| html += '<li>• Current state will be backed up automatically</li>'; |
| html += '<li>• You will need to recreate virtual environments after restore</li>'; |
| html += '</ul>'; |
| html += '</div>'; |
| |
| html += '</div>'; |
| |
| content.innerHTML = html; |
| modal.style.display = 'flex'; |
| } |
| |
| function closeRestoreModal() { |
| const modal = document.getElementById('restore-preview-modal'); |
| modal.style.display = 'none'; |
| } |
| |
| function restartWorkers() { |
| const btn = document.getElementById('restart-workers-btn'); |
| const resultDiv = document.getElementById('restart-result'); |
| |
| if (!confirm('Are you sure you want to restart the workers? Running tasks will complete before restart.')) { |
| return; |
| } |
| |
| btn.disabled = true; |
| btn.innerHTML = '<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg> Restarting...'; |
| resultDiv.classList.add('hidden'); |
| |
| fetch('{% url "cpanel:restart_workers" %}', { |
| method: 'POST', |
| headers: { |
| 'X-CSRFToken': '{{ csrf_token }}', |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| resultDiv.classList.remove('hidden'); |
| if (data.success) { |
| resultDiv.className = 'mt-4 p-4 rounded-lg bg-code-green/10 border border-code-green text-code-green'; |
| resultDiv.innerHTML = '<p class="font-medium">Success!</p><p class="text-sm mt-1">' + escapeHtml(data.message || 'Workers restarted successfully.') + '</p>'; |
| |
| setTimeout(loadSystemInfo, 3000); |
| } else { |
| resultDiv.className = 'mt-4 p-4 rounded-lg bg-code-red/10 border border-code-red text-code-red'; |
| resultDiv.innerHTML = '<p class="font-medium">Error</p><p class="text-sm mt-1">' + escapeHtml(data.error || 'Unknown error') + '</p>'; |
| } |
| }) |
| .catch(error => { |
| resultDiv.classList.remove('hidden'); |
| resultDiv.className = 'mt-4 p-4 rounded-lg bg-code-red/10 border border-code-red text-code-red'; |
| resultDiv.innerHTML = '<p class="font-medium">Error</p><p class="text-sm mt-1">' + escapeHtml(error.message) + '</p>'; |
| }) |
| .finally(() => { |
| btn.disabled = false; |
| btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg> Restart'; |
| }); |
| } |
| |
| |
| document.addEventListener('keydown', function(event) { |
| if (event.key === 'Escape') { |
| closeRestoreModal(); |
| } |
| }); |
| |
| |
| document.getElementById('restore-preview-modal').addEventListener('click', function(event) { |
| if (event.target === this) { |
| closeRestoreModal(); |
| } |
| }); |
| |
| |
| function toggleWeeklyDayVisibility() { |
| const scheduleSelect = document.getElementById('id_s3_backup_schedule'); |
| const weeklyDayContainer = document.getElementById('weekly-day-container'); |
| if (scheduleSelect && weeklyDayContainer) { |
| weeklyDayContainer.style.display = scheduleSelect.value === 'weekly' ? 'block' : 'none'; |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| toggleWeeklyDayVisibility(); |
| const scheduleSelect = document.getElementById('id_s3_backup_schedule'); |
| if (scheduleSelect) { |
| scheduleSelect.addEventListener('change', toggleWeeklyDayVisibility); |
| } |
| }); |
| |
| function runBackupNow() { |
| if (!confirm('Run a backup to S3 now?')) { |
| return; |
| } |
| |
| fetch('{% url "cpanel:backup_run_now" %}', { |
| method: 'POST', |
| headers: { |
| 'X-CSRFToken': '{{ csrf_token }}', |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| alert('Backup task queued successfully! Check back shortly for the result.'); |
| |
| setTimeout(() => window.location.reload(), 2000); |
| } else { |
| alert('Error: ' + (data.error || 'Unknown error')); |
| } |
| }) |
| .catch(error => { |
| alert('Error: ' + error.message); |
| }); |
| } |
| |
| |
| const hash = window.location.hash; |
| if (hash === '#security') { |
| showTab('security'); |
| } else if (hash === '#notifications') { |
| showTab('notifications'); |
| } else if (hash === '#schedules') { |
| showTab('schedules'); |
| } else if (hash === '#retention') { |
| showTab('retention'); |
| } else if (hash === '#workers') { |
| showTab('workers'); |
| } else if (hash === '#system') { |
| showTab('system'); |
| } else if (hash === '#backup') { |
| showTab('backup'); |
| } else if (hash === '#api') { |
| showTab('api'); |
| } else { |
| showTab('general'); |
| } |
| </script> |
| {% endblock %} |
|
|