| {% extends "base.html" %} |
|
|
| {% block title %}Services - PyRunner{% endblock %} |
|
|
| {% block content %} |
| <div class="flex"> |
| {% include "cpanel/_sidebar.html" %} |
|
|
| <div class="flex-1 p-8"> |
| |
| <div class="mb-8"> |
| <h1 class="text-2xl font-bold text-code-text">Services</h1> |
| <p class="text-code-muted">Configure external service integrations</p> |
| </div> |
|
|
| |
| {% if messages %} |
| <div class="mb-6 space-y-2"> |
| {% for message in messages %} |
| <div class="p-4 rounded-lg {% if message.tags == 'error' %}bg-code-red/10 border border-code-red text-code-red{% elif message.tags == 'success' %}bg-code-green/10 border border-code-green text-code-green{% else %}bg-code-accent/10 border border-code-accent text-code-accent{% endif %}"> |
| {{ message }} |
| </div> |
| {% endfor %} |
| </div> |
| {% endif %} |
|
|
| |
| <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden mb-6"> |
| |
| <div class="p-6 border-b border-code-border"> |
| <div class="flex items-center justify-between"> |
| <div class="flex items-center space-x-4"> |
| |
| <div class="w-12 h-12 bg-code-accent/20 rounded-lg flex items-center justify-center"> |
| <svg class="w-6 h-6 text-code-accent" 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> |
| </div> |
| <div> |
| <h2 class="text-lg font-semibold text-code-text">S3 Storage</h2> |
| <p class="text-sm text-code-muted">S3-compatible storage for scheduled backups</p> |
| </div> |
| </div> |
|
|
| |
| <div class="flex items-center space-x-3"> |
| {% if s3_status.enabled and s3_status.configured %} |
| <span class="px-3 py-1 bg-code-green/20 text-code-green text-sm font-medium rounded-full flex items-center"> |
| <span class="w-2 h-2 bg-code-green rounded-full mr-2"></span> |
| Active |
| </span> |
| {% elif s3_status.configured %} |
| <span class="px-3 py-1 bg-code-yellow/20 text-code-yellow text-sm font-medium rounded-full flex items-center"> |
| <span class="w-2 h-2 bg-code-yellow rounded-full mr-2"></span> |
| Configured (Disabled) |
| </span> |
| {% else %} |
| <span class="px-3 py-1 bg-code-muted/20 text-code-muted text-sm font-medium rounded-full flex items-center"> |
| <span class="w-2 h-2 bg-code-muted rounded-full mr-2"></span> |
| Not Configured |
| </span> |
| {% endif %} |
|
|
| |
| <button type="button" |
| onclick="toggleS3Config()" |
| id="s3-toggle-btn" |
| class="p-2 text-code-muted hover:text-code-text transition-colors"> |
| <svg id="s3-chevron" class="w-5 h-5 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/> |
| </svg> |
| </button> |
| </div> |
| </div> |
|
|
| |
| {% if s3_status.configured %} |
| <div class="mt-4 flex flex-wrap gap-3"> |
| <div class="flex items-center gap-2 px-3 py-1.5 bg-code-border/30 rounded-lg"> |
| <span class="text-code-muted text-xs uppercase tracking-wide">Bucket</span> |
| <span class="text-code-text font-mono text-sm">{{ s3_status.bucket }}</span> |
| </div> |
| <div class="flex items-center gap-2 px-3 py-1.5 bg-code-border/30 rounded-lg"> |
| <span class="text-code-muted text-xs uppercase tracking-wide">Endpoint</span> |
| <span class="text-code-text font-mono text-sm truncate max-w-xs">{{ s3_status.endpoint }}</span> |
| </div> |
| {% if s3_status.last_tested %} |
| <div class="flex items-center gap-2 px-3 py-1.5 bg-code-border/30 rounded-lg"> |
| <span class="text-code-muted text-xs uppercase tracking-wide">Last tested</span> |
| <span class="text-code-text text-sm">{{ s3_status.last_tested|timesince }} ago</span> |
| </div> |
| {% endif %} |
| </div> |
| {% endif %} |
| </div> |
|
|
| |
| <div id="s3-config" class="hidden"> |
| <form method="post" action="{% url 'cpanel:s3_settings' %}" class="p-6"> |
| {% csrf_token %} |
|
|
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> |
| |
| <div class="md:col-span-2"> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ s3_form.s3_endpoint_url.label }}</label> |
| {{ s3_form.s3_endpoint_url }} |
| <p class="mt-1 text-xs text-code-muted">{{ s3_form.s3_endpoint_url.help_text }}</p> |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ s3_form.s3_region.label }}</label> |
| {{ s3_form.s3_region }} |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ s3_form.s3_bucket_name.label }}</label> |
| {{ s3_form.s3_bucket_name }} |
| {% if s3_form.s3_bucket_name.errors %} |
| <p class="mt-1 text-sm text-code-red">{{ s3_form.s3_bucket_name.errors.0 }}</p> |
| {% endif %} |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ s3_form.s3_access_key.label }}</label> |
| {{ s3_form.s3_access_key }} |
| {% if settings.s3_access_key_encrypted %} |
| <p class="mt-1 text-xs text-code-green">Access key is configured</p> |
| {% endif %} |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-medium text-code-text mb-2">{{ s3_form.s3_secret_key.label }}</label> |
| {{ s3_form.s3_secret_key }} |
| {% if settings.s3_secret_key_encrypted %} |
| <p class="mt-1 text-xs text-code-green">Secret key is configured</p> |
| {% endif %} |
| </div> |
| </div> |
|
|
| |
| <div class="flex flex-wrap gap-6 mb-6 p-4 bg-code-bg rounded-lg"> |
| <label class="flex items-center cursor-pointer"> |
| {{ s3_form.s3_use_ssl }} |
| <span class="ml-2 text-sm text-code-text">{{ s3_form.s3_use_ssl.label }}</span> |
| </label> |
|
|
| <label class="flex items-center cursor-pointer"> |
| {{ s3_form.s3_path_style }} |
| <span class="ml-2 text-sm text-code-text">{{ s3_form.s3_path_style.label }}</span> |
| </label> |
|
|
| <label class="flex items-center cursor-pointer"> |
| {{ s3_form.s3_enabled }} |
| <span class="ml-2 text-sm text-code-text">{{ s3_form.s3_enabled.label }}</span> |
| </label> |
| </div> |
|
|
| |
| <div class="mb-6 p-4 bg-code-bg rounded-lg"> |
| <p class="text-sm font-medium text-code-text mb-3">Quick Setup Presets</p> |
| <div class="flex flex-wrap gap-2"> |
| <button type="button" onclick="setPreset('aws')" |
| class="px-3 py-1.5 text-xs bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded transition-colors"> |
| AWS S3 |
| </button> |
| <button type="button" onclick="setPreset('minio')" |
| class="px-3 py-1.5 text-xs bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded transition-colors"> |
| MinIO |
| </button> |
| <button type="button" onclick="setPreset('digitalocean')" |
| class="px-3 py-1.5 text-xs bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded transition-colors"> |
| DigitalOcean Spaces |
| </button> |
| <button type="button" onclick="setPreset('backblaze')" |
| class="px-3 py-1.5 text-xs bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded transition-colors"> |
| Backblaze B2 |
| </button> |
| <button type="button" onclick="setPreset('cloudflare')" |
| class="px-3 py-1.5 text-xs bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded transition-colors"> |
| Cloudflare R2 |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="flex items-center space-x-4"> |
| <button type="submit" class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors"> |
| Save Settings |
| </button> |
| <button type="button" |
| onclick="testS3Connection()" |
| id="test-s3-btn" |
| class="px-6 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors"> |
| Test Connection |
| </button> |
| </div> |
| </form> |
|
|
| |
| <div id="s3-test-result" class="hidden mx-8 mb-6 p-4 rounded-lg"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-code-surface/50 border border-code-border border-dashed rounded-xl p-8 text-center"> |
| <div class="w-12 h-12 bg-code-muted/10 rounded-lg flex items-center justify-center mx-auto mb-4"> |
| <svg class="w-6 h-6 text-code-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/> |
| </svg> |
| </div> |
| <h3 class="text-code-muted font-medium mb-2">More Services Coming Soon</h3> |
| <p class="text-sm text-code-muted/70">Additional integrations like SMTP, webhooks, and more will be added here.</p> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
| |
| function toggleS3Config() { |
| const config = document.getElementById('s3-config'); |
| const chevron = document.getElementById('s3-chevron'); |
| |
| config.classList.toggle('hidden'); |
| chevron.classList.toggle('rotate-180'); |
| } |
| |
| function testS3Connection() { |
| const btn = document.getElementById('test-s3-btn'); |
| const resultDiv = document.getElementById('s3-test-result'); |
| |
| btn.disabled = true; |
| btn.textContent = 'Testing...'; |
| resultDiv.classList.add('hidden'); |
| |
| |
| const formData = { |
| s3_endpoint_url: document.querySelector('[name="s3_endpoint_url"]').value, |
| s3_region: document.querySelector('[name="s3_region"]').value, |
| s3_bucket_name: document.querySelector('[name="s3_bucket_name"]').value, |
| s3_access_key: document.querySelector('[name="s3_access_key"]').value, |
| s3_secret_key: document.querySelector('[name="s3_secret_key"]').value, |
| s3_use_ssl: document.querySelector('[name="s3_use_ssl"]').checked, |
| s3_path_style: document.querySelector('[name="s3_path_style"]').checked, |
| }; |
| |
| fetch('{% url "cpanel:s3_test_connection" %}', { |
| method: 'POST', |
| headers: { |
| 'X-CSRFToken': '{{ csrf_token }}', |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(formData), |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| resultDiv.classList.remove('hidden'); |
| if (data.success) { |
| resultDiv.className = 'mx-8 mb-6 p-4 rounded-lg bg-code-green/10 border border-code-green text-code-green'; |
| resultDiv.innerHTML = '<div class="flex items-center"><svg class="w-5 h-5 mr-2 flex-shrink-0" 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><span>' + escapeHtml(data.message) + '</span></div>'; |
| } else { |
| resultDiv.className = 'mx-8 mb-6 p-4 rounded-lg bg-code-red/10 border border-code-red text-code-red'; |
| resultDiv.innerHTML = '<div class="flex items-center"><svg class="w-5 h-5 mr-2 flex-shrink-0" 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"/></svg><span>' + escapeHtml(data.error) + '</span></div>'; |
| } |
| }) |
| .catch(error => { |
| resultDiv.classList.remove('hidden'); |
| resultDiv.className = 'mx-8 mb-6 p-4 rounded-lg bg-code-red/10 border border-code-red text-code-red'; |
| resultDiv.textContent = 'Error: ' + escapeHtml(error.message); |
| }) |
| .finally(() => { |
| btn.disabled = false; |
| btn.textContent = 'Test Connection'; |
| }); |
| } |
| |
| function setPreset(provider) { |
| const endpoint = document.querySelector('[name="s3_endpoint_url"]'); |
| const region = document.querySelector('[name="s3_region"]'); |
| const useSSL = document.querySelector('[name="s3_use_ssl"]'); |
| const pathStyle = document.querySelector('[name="s3_path_style"]'); |
| |
| switch(provider) { |
| case 'aws': |
| endpoint.value = ''; |
| region.value = 'us-east-1'; |
| useSSL.checked = true; |
| pathStyle.checked = false; |
| break; |
| case 'minio': |
| endpoint.value = 'http://localhost:9000'; |
| region.value = 'us-east-1'; |
| useSSL.checked = false; |
| pathStyle.checked = true; |
| break; |
| case 'digitalocean': |
| endpoint.value = 'https://nyc3.digitaloceanspaces.com'; |
| region.value = 'nyc3'; |
| useSSL.checked = true; |
| pathStyle.checked = false; |
| break; |
| case 'backblaze': |
| endpoint.value = 'https://s3.us-west-002.backblazeb2.com'; |
| region.value = 'us-west-002'; |
| useSSL.checked = true; |
| pathStyle.checked = false; |
| break; |
| case 'cloudflare': |
| endpoint.value = 'https://<account_id>.r2.cloudflarestorage.com'; |
| region.value = 'auto'; |
| useSSL.checked = true; |
| pathStyle.checked = false; |
| break; |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| {% if s3_form.errors or not s3_status.configured %} |
| toggleS3Config(); |
| {% endif %} |
| }); |
| </script> |
| {% endblock %} |
|
|