feat: ssh auth, protocol management, and cleanup
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}{{ editing ? t('protocols.edit_protocol') : t('protocols.create_protocol') }} - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ editing ? t('protocols.edit_protocol') : t('protocols.create_protocol') }}</h1>
|
||||
<p class="mt-2 text-gray-600">{{ editing ? t('protocols.edit_protocol_description') : t('protocols.create_protocol_description') }}</p>
|
||||
</div>
|
||||
<a href="/settings/protocols" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
{{ t('protocols.back_to_protocols') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
{% if success %}
|
||||
<div class="mb-4 bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-green-800">{{ success }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-red-800">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Protocol Form -->
|
||||
<form id="protocol-form" method="POST" action="/settings/protocols/save" class="space-y-6">
|
||||
{% if editing %}
|
||||
<input type="hidden" name="id" value="{{ editing.id }}">
|
||||
{% endif %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('protocols.basic_information') }}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.name_label') }} *</label>
|
||||
<input type="text" id="name" name="name" value="{{ editing.name ?? '' }}" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.name_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.slug_label') }} *</label>
|
||||
<input type="text" id="slug" name="slug" value="{{ editing.slug ?? '' }}" required pattern="[a-z0-9_-]+" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.slug_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.description') }}</label>
|
||||
<textarea id="description" name="description" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">{{ editing.description ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.description_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installation Script -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.installation_script') }}</h2>
|
||||
<button type="button" id="ai-help-btn" class="inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea id="install_script" name="install_script" rows="15" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="#!/bin/bash # Installation script here">{{ editing.install_script ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.install_script_help') }}</p>
|
||||
<div class="mt-3 flex items-center space-x-2">
|
||||
<button id="test-install-btn" type="button" class="inline-flex items-center px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
<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="M9 12l2 2 4-4"/></svg>
|
||||
{{ t('protocols.test_install') }}
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">{{ t('protocols.testing_on_ubuntu22') }}</span>
|
||||
</div>
|
||||
<div id="test-install-result" class="mt-3 hidden">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ t('protocols.test_result') }}</h3>
|
||||
<pre id="test-install-output" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
|
||||
<h3 class="mt-3 text-sm font-medium text-gray-900">{{ t('protocols.client_output_preview') }}</h3>
|
||||
<pre id="test-client-preview" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uninstallation Script -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.uninstallation_script') }}</h2>
|
||||
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="uninstall">
|
||||
<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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="uninstall_script" name="uninstall_script" rows="12" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="#!/bin/bash # Uninstallation script here">{{ editing.uninstall_script ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.uninstall_script_help') }}</p>
|
||||
<div class="mt-3 flex items-center space-x-2">
|
||||
<button id="test-uninstall-btn" type="button" class="inline-flex items-center px-3 py-2 bg-red-600 text-white rounded hover:bg-red-700">
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
{{ t('protocols.test_uninstall') }}
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">{{ t('protocols.testing_on_ubuntu22') }}</span>
|
||||
</div>
|
||||
<div id="test-uninstall-result" class="mt-3 hidden">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ t('protocols.test_result') }}</h3>
|
||||
<pre id="test-uninstall-output" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Template -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.output_template') }}</h2>
|
||||
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="template">
|
||||
<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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea id="output_template" name="output_template" rows="10" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="[Interface] PrivateKey = {{private_key}} Address = {{client_ip}}/32">{{ editing.output_template ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.output_template_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 rounded-md">
|
||||
<h3 class="text-sm font-medium text-blue-900 mb-2">{{ t('protocols.available_variables') }}</h3>
|
||||
<div class="text-sm text-blue-800 space-y-1">
|
||||
<p><code>{{private_key}}</code> - {{ t('protocols.variable_private_key_help') }}</p>
|
||||
<p><code>{{public_key}}</code> - {{ t('protocols.variable_public_key_help') }}</p>
|
||||
<p><code>{{client_ip}}</code> - {{ t('protocols.variable_client_ip_help') }}</p>
|
||||
<p><code>{{server_host}}</code> - {{ t('protocols.variable_server_host_help') }}</p>
|
||||
<p><code>{{server_port}}</code> - {{ t('protocols.variable_server_port_help') }}</p>
|
||||
<p><code>{{preshared_key}}</code> - {{ t('protocols.variable_preshared_key_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Template -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="qr_section_toggle" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-3" checked>
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.qr_code_template') }}</h2>
|
||||
</div>
|
||||
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="qr_template">
|
||||
<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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="qr_section_content">
|
||||
<div class="mb-4">
|
||||
<label for="qr_code_format" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.qr_code_format') }}</label>
|
||||
<select id="qr_code_format" name="qr_code_format" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="amnezia_compressed" {% if editing.qr_code_format == 'amnezia_compressed' %}selected{% endif %}>Amnezia Compressed (Default)</option>
|
||||
<option value="raw" {% if editing.qr_code_format == 'raw' %}selected{% endif %}>Raw Content</option>
|
||||
<option value="text" {% if editing.qr_code_format == 'text' %}selected{% endif %}>{{ t('protocols.qr_code_format_text') }}</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.qr_code_format_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea id="qr_code_template" name="qr_code_template" rows="10" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{"last_config":{{last_config_json}}}">{{ editing.qr_code_template ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.qr_code_template_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 rounded-md">
|
||||
<h3 class="text-sm font-medium text-blue-900 mb-2">{{ t('protocols.available_variables') }}</h3>
|
||||
<div class="text-sm text-blue-800 space-y-1">
|
||||
<p><code>{{last_config_json}}</code> - {{ t('protocols.variable_last_config_json_help') }}</p>
|
||||
<p>{{ t('protocols.plus_all_output_variables') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Generation -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('protocols.password_generation') }}</h2>
|
||||
<div>
|
||||
<textarea id="password_command" name="password_command" rows="6" class="w-full px-3 py-2 border rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="echo \$(openssl rand -base64 12)">{{ editing.password_command ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.password_command_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('common.settings') }}</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="ubuntu_compatible" name="ubuntu_compatible" value="1" {% if editing.ubuntu_compatible %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="ubuntu_compatible" class="ml-2 block text-sm text-gray-900">{{ t('protocols.ubuntu_compatible') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="show_text_content" name="show_text_content" value="1" {% if editing.show_text_content %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="show_text_content" class="ml-2 block text-sm text-gray-900">{{ t('protocols.show_text_content') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="is_active" name="is_active" value="1" {% if editing.is_active is not defined or editing.is_active %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="is_active" class="ml-2 block text-sm text-gray-900">{{ t('protocols.active_label') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="/settings/protocols" class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{{ t('common.cancel') }}
|
||||
</a>
|
||||
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{{ editing ? t('protocols.update_protocol') : t('protocols.create_protocol') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Assistant Modal -->
|
||||
<div id="ai-assistant-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ t('ai.assistant') }}</h3>
|
||||
<button id="close-ai-modal" class="text-gray-400 hover:text-gray-600">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.select_model') }}</label>
|
||||
<select id="ai-model-select" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="openai/gpt-3.5-turbo">{{ t('ai.model_gpt35_turbo') }}</option>
|
||||
<option value="openai/gpt-4">{{ t('ai.model_gpt4') }}</option>
|
||||
<option value="anthropic/claude-3-haiku">{{ t('ai.model_claude3_haiku') }}</option>
|
||||
<option value="anthropic/claude-3-sonnet">{{ t('ai.model_claude3_sonnet') }}</option>
|
||||
</select>
|
||||
<div class="mt-2 flex items-center space-x-2">
|
||||
<input id="ai-model-custom" type="text" placeholder="{{ t('ai.custom_model_placeholder') }}" class="flex-1 px-3 py-2 border border-gray-300 rounded-md">
|
||||
<button id="ai-model-test-btn" type="button" class="px-3 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50">{{ t('ai.check_availability') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<a href="https://openrouter.ai/models" target="_blank" class="text-purple-600">openrouter.ai/models</a>
|
||||
</p>
|
||||
{% if not openrouter_key %}
|
||||
<div class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-yellow-800">{{ t('settings.no_api_key') }}</span>
|
||||
<a href="/settings#api" class="px-2 py-1 text-xs bg-purple-600 text-white rounded">{{ t('settings.enter_api_key') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.protocol_type') }}</label>
|
||||
<select id="ai-protocol-type" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">{{ t('ai.general_vpn') }}</option>
|
||||
<option value="wireguard">WireGuard</option>
|
||||
<option value="openvpn">OpenVPN</option>
|
||||
<option value="shadowsocks">Shadowsocks</option>
|
||||
<option value="cloak">Cloak</option>
|
||||
<option value="ikev2">IKEv2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.describe_requirements') }}</label>
|
||||
<textarea id="ai-prompt" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{{ t('ai.prompt_placeholder') }}"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button id="generate-script-btn" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<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="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
{{ t('ai.generate_script') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="ai-loading" class="hidden text-center py-4">
|
||||
<div class="inline-flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ t('ai.generating_script') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ai-result" class="hidden">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.generated_script') }}</label>
|
||||
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
|
||||
<pre id="generated-script" class="text-sm whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ai-suggestions" class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.suggestions') }}</label>
|
||||
<ul id="suggestions-list" class="list-disc list-inside space-y-1 text-sm text-gray-600"></ul>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.compatibility') }}</label>
|
||||
<div id="ubuntu-compatibility" class="flex items-center"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button id="apply-to-current-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
{{ t('ai.apply_to_current_protocol') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// AI Assistant Modal
|
||||
const aiModal = document.getElementById('ai-assistant-modal');
|
||||
const aiHelpBtns = document.querySelectorAll('.ai-help-btn');
|
||||
const closeAIModal = document.getElementById('close-ai-modal');
|
||||
const generateScriptBtn = document.getElementById('generate-script-btn');
|
||||
const aiLoading = document.getElementById('ai-loading');
|
||||
const aiResult = document.getElementById('ai-result');
|
||||
const applyToCurrentBtn = document.getElementById('apply-to-current-btn');
|
||||
const installScriptTextarea = document.getElementById('install_script');
|
||||
const uninstallScriptTextarea = document.getElementById('uninstall_script');
|
||||
const outputTemplateTextarea = document.getElementById('output_template');
|
||||
|
||||
// QR Template Section Toggle
|
||||
const qrSectionToggle = document.getElementById('qr_section_toggle');
|
||||
const qrSectionContent = document.getElementById('qr_section_content');
|
||||
|
||||
if (qrSectionToggle && qrSectionContent) {
|
||||
qrSectionToggle.addEventListener('change', function() {
|
||||
qrSectionContent.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
let currentAiTarget = 'install'; // install, uninstall, template
|
||||
|
||||
function showAIModal(target) {
|
||||
currentAiTarget = target || 'install';
|
||||
aiModal.classList.remove('hidden');
|
||||
aiResult.classList.add('hidden');
|
||||
aiLoading.classList.add('hidden');
|
||||
|
||||
// Update modal title or prompt placeholder based on target if needed
|
||||
const promptArea = document.getElementById('ai-prompt');
|
||||
if (currentAiTarget === 'template') {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder_template') }}";
|
||||
} else if (currentAiTarget === 'qr_template') {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder_qr_template') }}";
|
||||
} else if (currentAiTarget === 'uninstall') {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder_uninstall') }}";
|
||||
} else {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder') }}";
|
||||
}
|
||||
}
|
||||
|
||||
function hideAIModal() {
|
||||
aiModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Attach event listeners to all AI help buttons
|
||||
aiHelpBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const target = this.getAttribute('data-target') || 'install';
|
||||
showAIModal(target);
|
||||
});
|
||||
});
|
||||
|
||||
// Also attach to the original ID if it exists (for backward compatibility or if I missed updating one)
|
||||
const originalAiBtn = document.getElementById('ai-help-btn');
|
||||
if (originalAiBtn) {
|
||||
originalAiBtn.addEventListener('click', function() {
|
||||
showAIModal('install');
|
||||
});
|
||||
}
|
||||
|
||||
closeAIModal.addEventListener('click', hideAIModal);
|
||||
|
||||
// Close modal when clicking outside
|
||||
aiModal.addEventListener('click', function(e) {
|
||||
if (e.target === aiModal) {
|
||||
hideAIModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Generate script with AI
|
||||
generateScriptBtn.addEventListener('click', async function() {
|
||||
const model = document.getElementById('ai-model-select').value;
|
||||
const customModel = document.getElementById('ai-model-custom').value.trim();
|
||||
const effectiveModel = customModel !== '' ? customModel : model;
|
||||
const protocolType = document.getElementById('ai-protocol-type').value;
|
||||
const prompt = document.getElementById('ai-prompt').value;
|
||||
|
||||
if (!prompt.trim()) {
|
||||
alert('{{ t('ai.please_enter_requirements') }}');
|
||||
return;
|
||||
}
|
||||
|
||||
aiLoading.classList.remove('hidden');
|
||||
generateScriptBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ai/assist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
model: effectiveModel,
|
||||
protocol_type: protocolType,
|
||||
target: currentAiTarget
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
displayAIResult(result.data);
|
||||
} else {
|
||||
alert('{{ t('ai.error_generating_script') }}: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('{{ t('ai.error_generating_script') }}: ' + error.message);
|
||||
} finally {
|
||||
aiLoading.classList.add('hidden');
|
||||
generateScriptBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function displayAIResult(data) {
|
||||
document.getElementById('generated-script').textContent = data.script;
|
||||
|
||||
const suggestionsList = document.getElementById('suggestions-list');
|
||||
suggestionsList.innerHTML = '';
|
||||
data.suggestions.forEach(suggestion => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = suggestion;
|
||||
suggestionsList.appendChild(li);
|
||||
});
|
||||
|
||||
const compatibilityDiv = document.getElementById('ubuntu-compatibility');
|
||||
if (data.ubuntu_compatible) {
|
||||
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg><span class="text-green-700">Compatible with Ubuntu 22.04-24.04</span>';
|
||||
} else {
|
||||
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg><span class="text-red-700">May not be compatible with Ubuntu 22.04-24.04</span>';
|
||||
}
|
||||
|
||||
aiResult.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Apply to current protocol
|
||||
applyToCurrentBtn.addEventListener('click', function() {
|
||||
const generatedScript = document.getElementById('generated-script').textContent;
|
||||
if (generatedScript && confirm('{{ t('ai.confirm_apply_script') }}')) {
|
||||
if (currentAiTarget === 'uninstall') {
|
||||
uninstallScriptTextarea.value = generatedScript;
|
||||
} else if (currentAiTarget === 'template') {
|
||||
outputTemplateTextarea.value = generatedScript;
|
||||
} else if (currentAiTarget === 'qr_template') {
|
||||
document.getElementById('qr_code_template').value = generatedScript;
|
||||
} else {
|
||||
installScriptTextarea.value = generatedScript;
|
||||
}
|
||||
hideAIModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('protocol-form').addEventListener('submit', function(e) {
|
||||
const name = document.getElementById('name').value.trim();
|
||||
const slug = document.getElementById('slug').value.trim();
|
||||
|
||||
if (!name || !slug) {
|
||||
e.preventDefault();
|
||||
alert('{{ t('protocols.please_fill_required_fields') }}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9_-]+$/i.test(slug)) {
|
||||
e.preventDefault();
|
||||
alert('{{ t('protocols.invalid_slug_format') }}');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-generate slug from name
|
||||
document.getElementById('name').addEventListener('blur', function() {
|
||||
const name = this.value.trim();
|
||||
const slugField = document.getElementById('slug');
|
||||
|
||||
if (name && !slugField.value.trim()) {
|
||||
slugField.value = name.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Install Script
|
||||
const testBtn = document.getElementById('test-install-btn');
|
||||
const testBox = document.getElementById('test-install-result');
|
||||
const testOut = document.getElementById('test-install-output');
|
||||
const clientPrev = document.getElementById('test-client-preview');
|
||||
|
||||
if (testBtn) {
|
||||
testBtn.addEventListener('click', function() {
|
||||
const protocolId = {{ editing ? editing.id : 'null' }};
|
||||
if (!protocolId) {
|
||||
return;
|
||||
}
|
||||
testBtn.disabled = true;
|
||||
testBtn.classList.add('opacity-50');
|
||||
testOut.textContent = '';
|
||||
clientPrev.textContent = '';
|
||||
testBox.classList.remove('hidden');
|
||||
|
||||
const appendCmd = (cmd) => {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'text-xs text-gray-800';
|
||||
line.innerHTML = `<span class="text-blue-600">$</span> <code>${cmd}</code>`;
|
||||
testOut.appendChild(line);
|
||||
};
|
||||
const appendOut = (text) => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'mt-1 p-2 bg-gray-100 border border-gray-200 rounded text-xs whitespace-pre-wrap';
|
||||
pre.textContent = text;
|
||||
testOut.appendChild(pre);
|
||||
};
|
||||
const setError = (msg) => {
|
||||
const err = document.createElement('div');
|
||||
err.className = 'mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700';
|
||||
err.textContent = msg;
|
||||
testOut.appendChild(err);
|
||||
};
|
||||
|
||||
let es;
|
||||
try {
|
||||
es = new EventSource(`/api/protocols/${protocolId}/test-install/stream`);
|
||||
} catch (e) {
|
||||
es = null;
|
||||
}
|
||||
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'start') {
|
||||
appendOut('{{ t('protocols.testing_on_ubuntu22') }}');
|
||||
} else if (data.type === 'cmd') {
|
||||
appendCmd(data.cmd);
|
||||
} else if (data.type === 'out') {
|
||||
appendOut(data.line);
|
||||
} else if (data.type === 'cmd_done') {
|
||||
if (data.rc !== 0) {
|
||||
setError('Command failed');
|
||||
}
|
||||
} else if (data.type === 'preview') {
|
||||
clientPrev.textContent = data.preview || '';
|
||||
} else if (data.type === 'done') {
|
||||
es.close();
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
} else if (data.type === 'error') {
|
||||
setError(data.error || 'Unknown error');
|
||||
es.close();
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
setError('Connection failed');
|
||||
};
|
||||
} else {
|
||||
// Fallback to non-stream if needed, but we implemented stream
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test Uninstall Script
|
||||
const testUninstallBtn = document.getElementById('test-uninstall-btn');
|
||||
const testUninstallBox = document.getElementById('test-uninstall-result');
|
||||
const testUninstallOut = document.getElementById('test-uninstall-output');
|
||||
|
||||
if (testUninstallBtn) {
|
||||
testUninstallBtn.addEventListener('click', function() {
|
||||
const protocolId = {{ editing ? editing.id : 'null' }};
|
||||
if (!protocolId) {
|
||||
return;
|
||||
}
|
||||
testUninstallBtn.disabled = true;
|
||||
testUninstallBtn.classList.add('opacity-50');
|
||||
testUninstallOut.textContent = '';
|
||||
testUninstallBox.classList.remove('hidden');
|
||||
|
||||
const appendCmd = (cmd) => {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'text-xs text-gray-800';
|
||||
line.innerHTML = `<span class="text-blue-600">$</span> <code>${cmd}</code>`;
|
||||
testUninstallOut.appendChild(line);
|
||||
};
|
||||
const appendOut = (text) => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'mt-1 p-2 bg-gray-100 border border-gray-200 rounded text-xs whitespace-pre-wrap';
|
||||
pre.textContent = text;
|
||||
testUninstallOut.appendChild(pre);
|
||||
};
|
||||
const setError = (msg) => {
|
||||
const err = document.createElement('div');
|
||||
err.className = 'mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700';
|
||||
err.textContent = msg;
|
||||
testUninstallOut.appendChild(err);
|
||||
};
|
||||
|
||||
let es;
|
||||
try {
|
||||
es = new EventSource(`/api/protocols/${protocolId}/test-uninstall/stream`);
|
||||
} catch (e) {
|
||||
es = null;
|
||||
}
|
||||
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'start') {
|
||||
appendOut('{{ t('protocols.testing_on_ubuntu22') }}');
|
||||
} else if (data.type === 'cmd') {
|
||||
appendCmd(data.cmd);
|
||||
} else if (data.type === 'out') {
|
||||
appendOut(data.line);
|
||||
} else if (data.type === 'cmd_done') {
|
||||
if (data.rc !== 0) {
|
||||
setError('Command failed');
|
||||
}
|
||||
} else if (data.type === 'done') {
|
||||
es.close();
|
||||
testUninstallBtn.disabled = false;
|
||||
testUninstallBtn.classList.remove('opacity-50');
|
||||
} else if (data.type === 'error') {
|
||||
setError(data.error || 'Unknown error');
|
||||
es.close();
|
||||
testUninstallBtn.disabled = false;
|
||||
testUninstallBtn.classList.remove('opacity-50');
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
testUninstallBtn.disabled = false;
|
||||
testUninstallBtn.classList.remove('opacity-50');
|
||||
setError('Connection failed');
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user