Download templates/cpanel/settings.html from Akoda35/PyRunner: direct link, hf CLI and curl.
- Browser
- Download file 72.3 kB
-
https://huggingface.co/spaces/Akoda35/PyRunner/resolve/main/templates/cpanel/settings.html
- Command line
-
hf download hf://spaces/Akoda35/PyRunner/templates/cpanel/settings.html
-
curl -L -o settings.html https://huggingface.co/spaces/Akoda35/PyRunner/resolve/main/templates/cpanel/settings.html
72.3 kB
| {% 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> | |
| <!-- Tabs --> | |
| <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"> | |
| </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> | |
| <!-- General Settings Tab --> | |
| <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> | |
| <!-- Security Tab --> | |
| <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> | |
| <!-- Schedule Control Tab --> | |
| <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> | |
| <!-- Notifications Tab --> | |
| <div id="panel-notifications" class="tab-panel hidden"> | |
| {% if settings.email_backend == 'disabled' %} | |
| <!-- Security Warning when email is 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 %} | |
| <!-- Email Backend Selection --> | |
| <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> | |
| <!-- SMTP Configuration --> | |
| <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> | |
| <!-- Resend Configuration --> | |
| <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> | |
| <!-- Default Notification Email --> | |
| <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> | |
| <!-- Actions --> | |
| <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> | |
| <!-- Test Email Result --> | |
| <div id="test-email-result" class="hidden mt-4 p-4 rounded-lg max-w-2xl"></div> | |
| </div> | |
| <!-- Log Retention Tab --> | |
| <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> | |
| <!-- Manual Cleanup Section --> | |
| <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 %} | |
| <!-- Cleanup Preview --> | |
| <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> | |
| <!-- Worker Settings Tab --> | |
| <div id="panel-workers" class="tab-panel hidden"> | |
| {% if settings.worker_restart_required %} | |
| <!-- Restart Required Warning --> | |
| <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 %} | |
| <!-- Worker Control Section --> | |
| <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> | |
| <!-- System Info Tab --> | |
| <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> | |
| <!-- Backup & Restore Tab --> | |
| <div id="panel-backup" class="tab-panel hidden"> | |
| <!-- Create Backup Section --> | |
| <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"> | |
| <!-- Backup Format --> | |
| <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> | |
| <!-- Restore Backup Section --> | |
| <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> | |
| <!-- Scheduled S3 Backups Section --> | |
| <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"> | |
| <!-- Enable Scheduled Backups --> | |
| <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> | |
| <!-- Schedule Frequency --> | |
| <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> | |
| <!-- Backup Time --> | |
| <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> | |
| <!-- Day of Week (for weekly) --> | |
| <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> | |
| <!-- S3 Path Prefix --> | |
| <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> | |
| <!-- Retention Count --> | |
| <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> | |
| <!-- Backup Content Options --> | |
| <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> | |
| <!-- Backup Status Display --> | |
| {% 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> | |
| <!-- API Tab --> | |
| <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> | |
| <!-- Restore Preview Modal --> | |
| <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"> | |
| <!-- Preview content will be inserted here --> | |
| </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> | |
| // HTML escape helper to prevent XSS | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function showTab(tabName) { | |
| // Hide all panels | |
| document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden')); | |
| // Reset all tabs to inactive state | |
| 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'); | |
| }); | |
| // Show selected panel | |
| document.getElementById('panel-' + tabName).classList.remove('hidden'); | |
| // Highlight selected tab | |
| 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'); | |
| // Load system info when tab is selected | |
| 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; | |
| // Whitelist allowed status classes | |
| 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'; | |
| }); | |
| } | |
| // Backup & Restore functions | |
| 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>'; | |
| // Refresh system info after a delay | |
| 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'; | |
| }); | |
| } | |
| // Close modal on escape key | |
| document.addEventListener('keydown', function(event) { | |
| if (event.key === 'Escape') { | |
| closeRestoreModal(); | |
| } | |
| }); | |
| // Close modal on background click | |
| document.getElementById('restore-preview-modal').addEventListener('click', function(event) { | |
| if (event.target === this) { | |
| closeRestoreModal(); | |
| } | |
| }); | |
| // Scheduled Backup Functions | |
| 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'; | |
| } | |
| } | |
| // Initialize weekly day visibility | |
| 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.'); | |
| // Reload to show updated status | |
| setTimeout(() => window.location.reload(), 2000); | |
| } else { | |
| alert('Error: ' + (data.error || 'Unknown error')); | |
| } | |
| }) | |
| .catch(error => { | |
| alert('Error: ' + error.message); | |
| }); | |
| } | |
| // Check URL hash for tab navigation | |
| 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 %} | |