PyRunner / templates /cpanel /settings.html
Akoda35's picture
Upload 6 files
83e0bf1 verified
Raw
History Blame Contribute Delete
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">
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>
<!-- 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 %}