Add project files

This commit is contained in:
infosave2007
2025-11-07 13:34:06 +03:00
parent 3402b19f2c
commit a33af60f2d
41 changed files with 8128 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
{% extends "layout.twig" %}
{% block title %}{{ client.name }}{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">{{ client.name }}</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="bg-white rounded shadow p-6">
<h3 class="font-bold mb-4">Client Configuration</h3>
<dl class="space-y-2 mb-4">
<div><dt class="text-sm text-gray-600">IP Address</dt><dd>{{ client.client_ip }}</dd></div>
<div><dt class="text-sm text-gray-600">Status</dt>
<dd>
{% if client.status == 'active' %}
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">Active</span>
{% else %}
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">Disabled</span>
{% endif %}
</dd>
</div>
<div><dt class="text-sm text-gray-600">Created</dt><dd>{{ client.created_at }}</dd></div>
</dl>
<div class="flex gap-2">
<a href="/clients/{{ client.id }}/download" class="gradient-bg text-white px-4 py-2 rounded">
<i class="fas fa-download"></i> Download Config
</a>
{% if client.status == 'active' %}
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded" onclick="return confirm('Revoke access for this client?')">
<i class="fas fa-ban"></i> Revoke
</button>
</form>
{% else %}
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
<button type="submit" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded">
<i class="fas fa-check"></i> Restore
</button>
</form>
{% endif %}
</div>
</div>
<div class="bg-white rounded shadow p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold">Traffic Statistics</h3>
<button onclick="syncStats({{ client.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
<dl class="space-y-2" id="stats">
<div><dt class="text-sm text-gray-600">Uploaded</dt><dd id="stat-sent">{{ client.bytes_sent|default(0)|number_format }} B</dd></div>
<div><dt class="text-sm text-gray-600">Downloaded</dt><dd id="stat-received">{{ client.bytes_received|default(0)|number_format }} B</dd></div>
<div><dt class="text-sm text-gray-600">Total</dt><dd id="stat-total">{{ (client.bytes_sent|default(0) + client.bytes_received|default(0))|number_format }} B</dd></div>
<div><dt class="text-sm text-gray-600">Last Handshake</dt>
<dd id="stat-last-seen">
{% if client.last_handshake %}
{{ client.last_handshake }}
{% else %}
<span class="text-gray-400">Never connected</span>
{% endif %}
</dd>
</div>
</dl>
</div>
</div>
{% if client.qr_code %}
<div class="bg-white rounded shadow p-6 text-center">
<h3 class="font-bold mb-4">QR Code</h3>
<img src="{{ client.qr_code }}" alt="QR Code" class="mx-auto" style="max-width: 300px; width: 100%; height: auto;">
<p class="text-sm text-gray-600 mt-2">Scan with Amnezia VPN app</p>
</div>
{% endif %}
</div>
<script>
async function syncStats(clientId) {
try {
const response = await fetch(`/clients/${clientId}/sync-stats`, {
method: 'POST'
});
const data = await response.json();
if (data.success && data.stats) {
// Update stats display
document.getElementById('stat-sent').textContent = data.stats.sent;
document.getElementById('stat-received').textContent = data.stats.received;
document.getElementById('stat-total').textContent = data.stats.total;
document.getElementById('stat-last-seen').innerHTML = data.stats.is_online
? '<span class="text-green-600"><i class="fas fa-circle text-xs"></i> ' + data.stats.last_seen + '</span>'
: data.stats.last_seen;
} else {
alert('Failed to sync stats: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Error: ' + error.message);
}
}
</script>
{% endblock %}
+134
View File
@@ -0,0 +1,134 @@
{% extends "layout.twig" %}
{% block title %}{{ t('dashboard.title') }} - {{ app_name }}{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">
<i class="fas fa-tachometer-alt text-purple-600"></i>
{{ t('dashboard.title') }}
</h1>
<p class="mt-2 text-gray-600">{{ t('dashboard.welcome') }}</p>
</div>
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<i class="fas fa-server text-2xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.total_servers') }}</p>
<p class="text-2xl font-bold text-gray-900">{{ servers|length }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-users text-2xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.total_clients') }}</p>
<p class="text-2xl font-bold text-gray-900">{{ clients|length }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<i class="fas fa-check-circle text-2xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.active_clients') }}</p>
<p class="text-2xl font-bold text-gray-900">
{{ servers|filter(s => s.status == 'active')|length }}
</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-bold text-gray-900 mb-4">
<i class="fas fa-bolt text-yellow-500"></i>
{{ t('dashboard.quick_actions') }}
</h2>
<div class="flex space-x-4">
<a href="/servers/create" class="gradient-bg text-white px-6 py-3 rounded-md hover:opacity-90 inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
{{ t('servers.add') }}
</a>
</div>
</div>
<!-- Recent Servers -->
{% if servers|length > 0 %}
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">
<i class="fas fa-server text-blue-600"></i>
{{ t('dashboard.recent_servers') }}
</h2>
</div>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.name') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.host') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.status') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.actions') }}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for server in servers|slice(0, 5) %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ server.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ server.host }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if server.status == 'active' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
{{ t('status.active') }}
</span>
{% elseif server.status == 'deploying' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
{{ t('status.deploying') }}
</span>
{% else %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
{{ server.status|capitalize }}
</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<a href="/servers/{{ server.id }}" class="text-purple-600 hover:text-purple-900">
<i class="fas fa-eye"></i> {{ t('servers.view') }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="bg-white rounded-lg shadow p-12 text-center">
<i class="fas fa-server text-gray-300 text-6xl mb-4"></i>
<h3 class="text-xl font-medium text-gray-900 mb-2">{{ t('dashboard.no_servers') }}</h3>
<p class="text-gray-500 mb-6">{{ t('dashboard.get_started') }}</p>
<a href="/servers/create" class="gradient-bg text-white px-6 py-3 rounded-md hover:opacity-90 inline-block">
<i class="fas fa-plus mr-2"></i>
{{ t('dashboard.add_first_server') }}
</a>
</div>
{% endif %}
</div>
{% endblock %}
+176
View File
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ app_name }}{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.mobile-menu {
display: none;
}
.mobile-menu.active {
display: block;
}
.language-dropdown {
display: none;
}
.language-dropdown.active {
display: block;
}
</style>
</head>
<body class="bg-gray-50">
{% if user %}
<!-- Navigation -->
<nav class="gradient-bg shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<div class="flex-shrink-0 flex items-center">
<i class="fas fa-shield-alt text-white text-2xl mr-2"></i>
<span class="text-white text-xl font-bold">{{ app_name }}</span>
</div>
<!-- Desktop Menu -->
<div class="hidden md:ml-6 md:flex md:space-x-8">
<a href="/dashboard" class="text-white hover:text-gray-200 inline-flex items-center px-1 pt-1 text-sm font-medium">
<i class="fas fa-tachometer-alt mr-2"></i>{{ t('menu.dashboard') }}
</a>
<a href="/servers" class="text-white hover:text-gray-200 inline-flex items-center px-1 pt-1 text-sm font-medium">
<i class="fas fa-server mr-2"></i>{{ t('menu.servers') }}
</a>
{% if user.role == 'admin' %}
<a href="/settings" class="text-white hover:text-gray-200 inline-flex items-center px-1 pt-1 text-sm font-medium">
<i class="fas fa-cog mr-2"></i>{{ t('menu.settings') }}
</a>
{% endif %}
</div>
</div>
<div class="flex items-center">
<!-- Language Selector -->
<div class="relative mr-4">
<button onclick="toggleLanguageDropdown()" class="text-white hover:text-gray-200 flex items-center focus:outline-none">
{% set currentLang = current_language %}
{% for lang in languages %}
{% if lang.code == currentLang %}
<span class="mr-2">{{ getFlag(lang.code) }}</span>
{% endif %}
{% endfor %}
<i class="fas fa-chevron-down ml-1 text-xs"></i>
</button>
<div id="languageDropdown" class="language-dropdown absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-50">
{% for lang in languages %}
<form method="POST" action="/language/change" class="block">
<input type="hidden" name="language" value="{{ lang.code }}">
<input type="hidden" name="redirect" value="{{ current_uri }}">
<button type="submit" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center
{% if lang.code == current_language %}bg-purple-50 font-medium{% endif %}">
<span class="mr-2">{{ getFlag(lang.code) }}</span>
<span>{{ lang.name }}</span>
{% if lang.code == current_language %}
<i class="fas fa-check text-purple-600 ml-auto"></i>
{% endif %}
</button>
</form>
{% endfor %}
</div>
</div>
<!-- User Menu (Desktop) -->
<div class="hidden md:flex items-center">
<span class="text-white mr-4">
<i class="fas fa-user mr-1"></i>{{ user.name }}
</span>
<a href="/logout" class="text-white hover:text-gray-200">
<i class="fas fa-sign-out-alt mr-1"></i>{{ t('menu.logout') }}
</a>
</div>
<!-- Mobile menu button -->
<div class="md:hidden flex items-center ml-4">
<button onclick="toggleMobileMenu()" type="button" class="text-white hover:text-gray-200 focus:outline-none">
<i class="fas fa-bars text-xl"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobileMenu" class="mobile-menu md:hidden">
<div class="px-2 pt-2 pb-3 space-y-1">
<a href="/dashboard" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
<i class="fas fa-tachometer-alt mr-2"></i>{{ t('menu.dashboard') }}
</a>
<a href="/servers" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
<i class="fas fa-server mr-2"></i>{{ t('menu.servers') }}
</a>
{% if user.role == 'admin' %}
<a href="/settings" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
<i class="fas fa-cog mr-2"></i>{{ t('menu.settings') }}
</a>
{% endif %}
<div class="border-t border-purple-500 my-2"></div>
<div class="text-white px-3 py-2">
<i class="fas fa-user mr-1"></i>{{ user.name }}
</div>
<a href="/logout" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
<i class="fas fa-sign-out-alt mr-1"></i>{{ t('menu.logout') }}
</a>
</div>
</div>
</nav>
{% endif %}
<!-- Main Content -->
<main class="{% if user %}py-10{% endif %}">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-white border-t border-gray-200 mt-auto">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<p class="text-center text-gray-500 text-sm">
{{ app_name }} &copy; 2025 | Open Source VPN Management Panel
</p>
</div>
</footer>
<script>
function toggleMobileMenu() {
const menu = document.getElementById('mobileMenu');
menu.classList.toggle('active');
}
function toggleLanguageDropdown() {
const dropdown = document.getElementById('languageDropdown');
dropdown.classList.toggle('active');
}
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
const languageButton = event.target.closest('button[onclick="toggleLanguageDropdown()"]');
const dropdown = document.getElementById('languageDropdown');
const isInsideDropdown = dropdown && dropdown.contains(event.target);
// Don't close if clicking the button or inside dropdown (except submit buttons)
if (!languageButton && !isInsideDropdown && dropdown) {
dropdown.classList.remove('active');
}
});
// Ensure dropdown stays open when form is being submitted
document.addEventListener('submit', function(event) {
if (event.target.closest('#languageDropdown')) {
// Form will submit normally, dropdown will close on page reload
}
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
{% extends "layout.twig" %}
{% block title %}Login - {{ app_name }}{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center gradient-bg py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<div class="flex justify-center">
<i class="fas fa-shield-alt text-white text-6xl"></i>
</div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-white">
{{ app_name }}
</h2>
<p class="mt-2 text-center text-sm text-gray-100">
Sign in to manage your VPN servers
</p>
</div>
<div class="bg-white rounded-lg shadow-xl p-8">
{% if error %}
<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<span class="block sm:inline">{{ error }}</span>
</div>
{% endif %}
<form class="space-y-6" method="POST" action="/login">
<div>
<label for="email" class="block text-sm font-medium text-gray-700">
Email address
</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-envelope text-gray-400"></i>
</div>
<input id="email" name="email" type="email" autocomplete="email" required
class="appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500"
placeholder="admin@amnez.ia">
</div>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">
Password
</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400"></i>
</div>
<input id="password" name="password" type="password" autocomplete="current-password" required
class="appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500"
placeholder="••••••••">
</div>
</div>
<div>
<button type="submit"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white gradient-bg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<i class="fas fa-sign-in-alt mr-2"></i>
Sign in
</button>
</div>
</form>
</div>
<div class="text-center text-xs text-white">
<p>Default credentials: admin@amnez.ia / admin123</p>
</div>
</div>
</div>
{% endblock %}
+57
View File
@@ -0,0 +1,57 @@
{% extends "layout.twig" %}
{% block title %}Register - {{ app_name }}{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center gradient-bg py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<div class="flex justify-center">
<i class="fas fa-user-plus text-white text-6xl"></i>
</div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-white">
Create Account
</h2>
</div>
<div class="bg-white rounded-lg shadow-xl p-8">
{% if error %}
<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded relative">
{{ error }}
</div>
{% endif %}
<form class="space-y-6" method="POST" action="/register">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Full Name</label>
<input id="name" name="name" type="text" required
class="mt-1 appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input id="email" name="email" type="email" required
class="mt-1 appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input id="password" name="password" type="password" required
class="mt-1 appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500">
<p class="mt-1 text-sm text-gray-500">Must be at least 6 characters</p>
</div>
<button type="submit" class="w-full gradient-bg text-white py-2 px-4 rounded-md hover:opacity-90">
Register
</button>
<div class="text-center text-sm">
<a href="/login" class="text-purple-600 hover:text-purple-500">
Already have an account? Sign in
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+16
View File
@@ -0,0 +1,16 @@
{% extends "layout.twig" %}
{% block title %}Add Server{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold mb-8"><i class="fas fa-plus-circle text-purple-600"></i> Add New Server</h1>
{% if error %}<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded">{{ error }}</div>{% endif %}
<form method="POST" class="bg-white shadow rounded-lg p-6 space-y-6">
<div><label class="block text-sm font-medium text-gray-700">Server Name</label><input name="name" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="US Server 1"></div>
<div><label class="block text-sm font-medium text-gray-700">Host IP/Domain</label><input name="host" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="123.456.789.0"></div>
<div><label class="block text-sm font-medium text-gray-700">SSH Port</label><input name="port" type="number" value="22" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
<div><label class="block text-sm font-medium text-gray-700">SSH Username</label><input name="username" value="root" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
<div><label class="block text-sm font-medium text-gray-700">SSH Password</label><input name="password" type="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
<button type="submit" class="w-full gradient-bg text-white py-2 px-4 rounded-md hover:opacity-90"><i class="fas fa-save mr-2"></i>Create Server</button>
</form>
</div>
{% endblock %}
+57
View File
@@ -0,0 +1,57 @@
{% extends "layout.twig" %}
{% block title %}Deploy {{ server.name }}{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">Deploying: {{ server.name }}</h1>
<div id="deployLog" class="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm h-96 overflow-y-auto mb-4">
<div>Ready to deploy...</div>
</div>
<button id="deployBtn" onclick="deploy()" class="gradient-bg text-white px-6 py-2 rounded hover:opacity-90 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
<span id="btnText">Start Deployment</span>
<i id="btnSpinner" class="fas fa-spinner fa-spin ml-2 hidden"></i>
</button>
</div>
<script>
function deploy() {
const btn = document.getElementById('deployBtn');
const btnText = document.getElementById('btnText');
const btnSpinner = document.getElementById('btnSpinner');
const log = document.getElementById('deployLog');
// Disable button and show spinner
btn.disabled = true;
btnText.textContent = 'Deploying...';
btnSpinner.classList.remove('hidden');
log.innerHTML = '<div>📡 Connecting to server...</div>';
log.innerHTML += '<div>🔧 Installing Docker...</div>';
log.innerHTML += '<div>📦 Building container...</div>';
log.innerHTML += '<div>🔐 Generating keys...</div>';
log.innerHTML += '<div>⚙️ Configuring WireGuard...</div>';
fetch('/servers/{{ server.id }}/deploy', {method: 'POST'})
.then(r => r.json())
.then(d => {
if (d.success) {
log.innerHTML += '<div class="text-green-500 font-bold">✅ Deployment successful!</div>';
log.innerHTML += '<div class="text-yellow-300">🔌 VPN Port: ' + d.vpn_port + '</div>';
log.innerHTML += '<div class="text-yellow-300">🔑 Public Key: ' + d.public_key.substring(0, 40) + '...</div>';
btnText.textContent = 'Redirecting...';
btnSpinner.classList.add('hidden');
setTimeout(() => window.location.href = '/servers/{{ server.id }}', 2000);
} else {
log.innerHTML += '<div class="text-red-500 font-bold">❌ Error: ' + (d.error || 'Unknown error') + '</div>';
btn.disabled = false;
btnText.textContent = 'Retry Deployment';
btnSpinner.classList.add('hidden');
}
})
.catch(e => {
log.innerHTML += '<div class="text-red-500 font-bold">❌ Network error: ' + e.message + '</div>';
btn.disabled = false;
btnText.textContent = 'Retry Deployment';
btnSpinner.classList.add('hidden');
});
}
</script>
{% endblock %}
+66
View File
@@ -0,0 +1,66 @@
{% extends "layout.twig" %}
{% block title %}{{ t('servers.title') }}{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">{{ t('servers.title') }}</h1>
<a href="/servers/create" class="gradient-bg text-white px-4 py-2 rounded"><i class="fas fa-plus"></i> {{ t('servers.add') }}</a>
</div>
{% if session.success_message %}
<div class="mb-4 bg-green-50 border-l-4 border-green-400 p-4">
<p class="text-sm text-green-700"><i class="fas fa-check-circle mr-2"></i>{{ session.success_message }}</p>
</div>
{% endif %}
{% if session.error_message %}
<div class="mb-4 bg-red-50 border-l-4 border-red-400 p-4">
<p class="text-sm text-red-700"><i class="fas fa-exclamation-circle mr-2"></i>{{ session.error_message }}</p>
</div>
{% endif %}
{% if servers|length > 0 %}
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.name') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.host') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.status') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.actions') }}</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr class="border-t">
<td class="px-6 py-4 font-medium">{{ server.name }}</td>
<td class="px-6 py-4">{{ server.host }}</td>
<td class="px-6 py-4">
<span class="px-2 py-1 bg-{{ server.status == 'active' ? 'green' : 'yellow' }}-100 text-{{ server.status == 'active' ? 'green' : 'yellow' }}-800 rounded text-sm">
{{ server.status }}
</span>
</td>
<td class="px-6 py-4 space-x-3">
<a href="/servers/{{ server.id }}" class="text-purple-600 hover:text-purple-900">
<i class="fas fa-eye mr-1"></i>{{ t('servers.view') }}
</a>
<form method="POST" action="/servers/{{ server.id }}/delete" class="inline"
onsubmit="return confirm('{{ t('message.confirm') }} Delete server {{ server.name }}?');">
<button type="submit" class="text-red-600 hover:text-red-900">
<i class="fas fa-trash mr-1"></i>{{ t('servers.delete') }}
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="bg-white rounded shadow p-12 text-center">
<p class="text-gray-500 mb-4">{{ t('dashboard.no_servers') }}</p>
<a href="/servers/create" class="gradient-bg text-white px-6 py-3 rounded inline-block">{{ t('dashboard.add_first_server') }}</a>
</div>
{% endif %}
</div>
{% endblock %}
+134
View File
@@ -0,0 +1,134 @@
{% extends "layout.twig" %}
{% block title %}{{ server.name }}{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="mb-6"><h1 class="text-3xl font-bold">{{ server.name }}</h1><p class="text-gray-600">{{ server.host }}</p></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded shadow p-6">
<h3 class="font-bold mb-4">Server Info</h3>
<dl class="space-y-2">
<div><dt class="text-sm text-gray-600">Status</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div>
<div><dt class="text-sm text-gray-600">VPN Port</dt><dd>{{ server.vpn_port }}</dd></div>
<div><dt class="text-sm text-gray-600">Subnet</dt><dd>{{ server.vpn_subnet }}</dd></div>
</dl>
</div>
<div class="bg-white rounded shadow p-6">
<h3 class="font-bold mb-4">Create Client</h3>
<form method="POST" action="/servers/{{ server.id }}/clients/create" class="flex gap-2" id="createClientForm">
<input name="name" placeholder="Client name" required class="flex-1 px-3 py-2 border rounded" id="clientName">
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded" id="createClientBtn">
<span id="createClientText">Create</span>
<i class="fas fa-spinner fa-spin" id="createClientSpinner" style="display:none;"></i>
</button>
</form>
</div>
</div>
<div class="bg-white rounded shadow">
<div class="px-6 py-4 border-b flex justify-between items-center">
<h3 class="font-bold">Clients ({{ clients|length }})</h3>
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
<i class="fas fa-sync-alt"></i> Sync Stats
</button>
</div>
{% if clients|length > 0 %}
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Traffic</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Last Seen</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody>
{% for client in clients %}
<tr class="border-t">
<td class="px-6 py-4">{{ client.name }}</td>
<td class="px-6 py-4">{{ client.client_ip }}</td>
<td class="px-6 py-4">
{% if client.status == 'active' %}
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">Active</span>
{% else %}
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">Disabled</span>
{% endif %}
</td>
<td class="px-6 py-4 text-sm">
<div class="text-gray-600">
{{ (client.bytes_sent|default(0) / 1024 / 1024)|number_format(2) }} MB
</div>
<div class="text-gray-600">
{{ (client.bytes_received|default(0) / 1024 / 1024)|number_format(2) }} MB
</div>
</td>
<td class="px-6 py-4 text-sm">
{% if client.last_handshake %}
<span class="text-gray-600">{{ client.last_handshake }}</span>
{% else %}
<span class="text-gray-400">Never</span>
{% endif %}
</td>
<td class="px-6 py-4">
<a href="/clients/{{ client.id }}" class="text-purple-600 hover:text-purple-800 mr-2">View</a>
{% if client.status == 'active' %}
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
<button type="submit" class="text-orange-600 hover:text-orange-800 mr-2" onclick="return confirm('Revoke access for this client?')">Revoke</button>
</form>
{% else %}
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
<button type="submit" class="text-green-600 hover:text-green-800 mr-2">Restore</button>
</form>
{% endif %}
<form method="POST" action="/clients/{{ client.id }}/delete" style="display:inline;">
<button type="submit" class="text-red-600 hover:text-red-800" onclick="return confirm('Delete this client permanently?')">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-12 text-center text-gray-500">No clients yet</div>
{% endif %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('createClientForm');
if (form) {
form.addEventListener('submit', function(e) {
const btn = document.getElementById('createClientBtn');
const text = document.getElementById('createClientText');
const spinner = document.getElementById('createClientSpinner');
// Show spinner and disable button
btn.disabled = true;
text.style.display = 'none';
spinner.style.display = 'inline-block';
// Form will submit normally
});
}
});
async function syncAllStats(serverId) {
try {
const response = await fetch(`/servers/${serverId}/sync-stats`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Failed to sync stats: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Error: ' + error.message);
}
}
</script>
{% endblock %}
+338
View File
@@ -0,0 +1,338 @@
{% extends "layout.twig" %}
{% block title %}{{ t('menu.settings') }} - {{ app_name }}{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">{{ t('menu.settings') }}</h1>
<p class="mt-2 text-sm text-gray-600">{{ t('settings.description') }}</p>
</div>
{% if success %}
<div class="mb-4 bg-green-50 border-l-4 border-green-400 p-4">
<div class="flex">
<i class="fas fa-check-circle text-green-400 mt-0.5"></i>
<p class="ml-3 text-sm text-green-700">{{ success }}</p>
</div>
</div>
{% endif %}
{% if error %}
<div class="mb-4 bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
<i class="fas fa-exclamation-circle text-red-400 mt-0.5"></i>
<p class="ml-3 text-sm text-red-700">{{ error }}</p>
</div>
</div>
{% endif %}
<!-- Tabs -->
<div class="mb-6 border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<a href="#" onclick="showTab('profile'); return false;" id="tab-profile"
class="tab-link border-purple-500 text-purple-600 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-user mr-2"></i>Profile
</a>
<a href="#" onclick="showTab('api'); return false;" id="tab-api"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-key mr-2"></i>{{ t('settings.api_keys') }}
</a>
<a href="#" onclick="showTab('translations'); return false;" id="tab-translations"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-language mr-2"></i>Translations
</a>
{% if user.role == 'admin' %}
<a href="#" onclick="showTab('users'); return false;" id="tab-users"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-users mr-2"></i>Users
</a>
{% endif %}
</nav>
</div>
<!-- Profile Tab -->
<div id="content-profile" class="tab-content">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<i class="fas fa-lock mr-2 text-purple-600"></i>Change Password
</h2>
</div>
<div class="px-6 py-5">
<form method="POST" action="/settings/change-password">
<div class="space-y-4 max-w-md">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Current Password</label>
<input type="password" name="current_password" required
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
<input type="password" name="new_password" required minlength="6"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
<p class="mt-1 text-xs text-gray-500">Minimum 6 characters</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Confirm Password</label>
<input type="password" name="confirm_password" required minlength="6"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
<i class="fas fa-save mr-2"></i>Change Password
</button>
</div>
</form>
</div>
</div>
</div>
<!-- API Tab -->
<div id="content-api" class="tab-content hidden">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<i class="fas fa-key mr-2 text-purple-600"></i>{{ t('settings.api_keys') }}
</h2>
</div>
<div class="px-6 py-5">
{% if openrouter_key %}
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-md">
<div class="flex items-center justify-between">
<div>
<i class="fas fa-check-circle text-green-600 mr-2"></i>
<span class="text-sm font-medium text-green-900">API Key Configured</span>
</div>
<code class="text-xs text-green-700 bg-green-100 px-2 py-1 rounded">
{{ openrouter_key[:15] }}...{{ openrouter_key[-4:] }}
</code>
</div>
</div>
{% else %}
<div class="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2"></i>
<span class="text-sm text-yellow-800">No API key configured. Auto-translation will not work.</span>
</div>
{% endif %}
<form method="POST" action="/settings/api-key">
<div class="max-w-md">
<label class="block text-sm font-medium text-gray-700 mb-2">
OpenRouter API Key <span class="text-gray-500 font-normal">({{ t('settings.for_translation') }})</span>
</label>
<input type="text" name="api_key" placeholder="sk-or-v1-..." value="{{ openrouter_key }}"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
<p class="mt-2 text-xs text-gray-500">
<i class="fas fa-info-circle"></i> {{ t('settings.get_key_at') }}
<a href="https://openrouter.ai/keys" target="_blank" class="text-purple-600">openrouter.ai/keys</a>
</p>
<input type="hidden" name="service" value="openrouter">
<button type="submit" class="mt-4 px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
<i class="fas fa-save mr-2"></i>{{ openrouter_key ? t('form.update') : t('form.save') }}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Translations Tab -->
<div id="content-translations" class="tab-content hidden">
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<i class="fas fa-language mr-2 text-purple-600"></i>{{ t('settings.translation_status') }}
</h2>
</div>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('settings.language') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('settings.progress') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('settings.actions') }}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for stat in translation_stats %}
<tr>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="text-2xl mr-3">{{ getFlag(stat.code) }}</span>
<div>
<div class="text-sm font-medium">{{ stat.name }}</div>
<div class="text-xs text-gray-500">({{ stat.code }})</div>
</div>
</div>
</td>
<td class="px-6 py-4">
{% set percent = (stat.translated_count / stat.total_count * 100)|round %}
<div class="flex items-center">
<div class="w-full bg-gray-200 rounded-full h-2 mr-2">
<div class="bg-purple-600 h-2 rounded-full" style="width: {{ percent }}%"></div>
</div>
<span class="text-sm">{{ percent }}%</span>
</div>
<div class="text-xs text-gray-500 mt-1">{{ stat.translated_count }} / {{ stat.total_count }}</div>
</td>
<td class="px-6 py-4 text-sm">
{% if stat.code != 'en' and stat.translated_count < stat.total_count %}
<button onclick="translateLanguage('{{ stat.code }}')" class="text-purple-600 hover:text-purple-900">
<i class="fas fa-robot mr-1"></i>{{ t('settings.auto_translate') }}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Users Tab -->
{% if user.role == 'admin' %}
<div id="content-users" class="tab-content hidden">
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<i class="fas fa-user-plus mr-2 text-purple-600"></i>Add User
</h2>
</div>
<div class="px-6 py-5">
<form method="POST" action="/settings/add-user">
<div class="grid grid-cols-2 gap-4 max-w-2xl">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input type="text" name="name" required class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input type="email" name="email" required class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input type="password" name="password" required minlength="6" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
<select name="role" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
<option value="user">User</option>
<option value="admin">Administrator</option>
</select>
</div>
</div>
<button type="submit" class="mt-4 px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
<i class="fas fa-user-plus mr-2"></i>Add User
</button>
</form>
</div>
</div>
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<i class="fas fa-users mr-2 text-purple-600"></i>All Users
</h2>
</div>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Role</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for u in users %}
<tr>
<td class="px-6 py-4">
<div class="flex items-center">
<i class="fas fa-user-circle text-gray-400 text-lg mr-2"></i>
<span class="text-sm font-medium">{{ u.name }}</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ u.email }}</td>
<td class="px-6 py-4">
{% if u.role == 'admin' %}
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">Admin</span>
{% else %}
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">User</span>
{% endif %}
</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ u.created_at|date('Y-m-d') }}</td>
<td class="px-6 py-4 text-sm">
{% if u.id != user.id %}
<form method="POST" action="/settings/delete-user/{{ u.id }}" class="inline" onsubmit="return confirm('Delete {{ u.name }}?')">
<button type="submit" class="text-red-600 hover:text-red-900">
<i class="fas fa-trash"></i> Delete
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
<script>
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-link').forEach(el => {
el.classList.remove('border-purple-500', 'text-purple-600');
el.classList.add('border-transparent', 'text-gray-500');
});
document.getElementById('content-' + tab).classList.remove('hidden');
const link = document.getElementById('tab-' + tab);
link.classList.remove('border-transparent', 'text-gray-500');
link.classList.add('border-purple-500', 'text-purple-600');
// Save active tab to URL hash
window.location.hash = tab;
}
// Restore active tab on page load
document.addEventListener('DOMContentLoaded', function() {
const hash = window.location.hash.substring(1);
if (hash && document.getElementById('tab-' + hash)) {
showTab(hash);
}
});
function translateLanguage(lang) {
if (!confirm('{{ t('settings.confirm_translate') }}')) return;
const button = event.target.closest('button');
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> {{ t('form.processing') }}';
fetch('/api/auth/token', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'email=admin@amnez.ia&password=admin123' })
.then(r => r.json())
.then(auth => fetch('/api/translations/auto-translate', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + auth.token},
body: JSON.stringify({language: lang})
}))
.then(r => r.json())
.then(data => {
if (data.success) {
alert('{{ t('settings.translation_complete') }}: ' + data.stats.translated + '/' + data.stats.total);
location.reload();
} else {
alert('{{ t('message.error') }}: ' + (data.error || 'Unknown error'));
button.disabled = false;
button.innerHTML = originalText;
}
})
.catch(err => {
alert('{{ t('message.error') }}: ' + err.message);
button.disabled = false;
button.innerHTML = originalText;
});
}
</script>
{% endblock %}
+199
View File
@@ -0,0 +1,199 @@
{% extends "layout.twig" %}
{% block title %}{{ t('menu.settings') }} - {{ app_name }}{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">{{ t('menu.settings') }}</h1>
<p class="mt-2 text-sm text-gray-600">{{ t('settings.description') }}</p>
</div>
{% if success %}
<div class="mb-4 bg-green-50 border-l-4 border-green-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-check-circle text-green-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-green-700">{{ success }}</p>
</div>
</div>
</div>
{% endif %}
{% if error %}
<div class="mb-4 bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-exclamation-circle text-red-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-red-700">{{ error }}</p>
</div>
</div>
</div>
{% endif %}
<div class="bg-white shadow rounded-lg">
<!-- API Keys Section -->
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<i class="fas fa-key mr-2 text-purple-600"></i>
{{ t('settings.api_keys') }}
</h2>
<p class="mt-1 text-sm text-gray-500">{{ t('settings.api_keys_desc') }}</p>
</div>
<div class="px-6 py-5">
<!-- OpenRouter API Key -->
<form method="POST" action="/settings/api-key">
<div class="mb-6">
<label for="openrouter_key" class="block text-sm font-medium text-gray-700 mb-2">
OpenRouter API Key
<span class="text-gray-400 font-normal ml-2">({{ t('settings.for_translation') }})</span>
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<input type="text"
name="api_key"
id="openrouter_key"
value=""
class="flex-1 min-w-0 block w-full px-3 py-2 rounded-l-md border border-gray-300 focus:ring-purple-500 focus:border-purple-500 sm:text-sm"
placeholder="sk-or-v1-...">
<button type="button"
onclick="togglePassword('openrouter_key')"
class="inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100">
<i class="fas fa-eye"></i>
</button>
</div>
<p class="mt-2 text-sm text-gray-500">
<i class="fas fa-info-circle mr-1"></i>
{{ t('settings.get_key_at') }} <a href="https://openrouter.ai/keys" target="_blank" class="text-purple-600 hover:text-purple-800">openrouter.ai/keys</a>
</p>
</div>
<input type="hidden" name="service" value="openrouter">
<div class="flex justify-end">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<i class="fas fa-save mr-2"></i>
{{ t('form.save') }}
</button>
</div>
</form>
</div>
<!-- Translation Section -->
<div class="px-6 py-5 border-t border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">
<i class="fas fa-language mr-2 text-purple-600"></i>
{{ t('settings.translation_status') }}
</h3>
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{{ t('settings.language') }}
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{{ t('settings.progress') }}
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{{ t('settings.actions') }}
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for stat in translation_stats %}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="text-2xl mr-3">{{ getFlag(stat.code) }}</span>
<div>
<div class="text-sm font-medium text-gray-900">{{ stat.name }}</div>
<div class="text-sm text-gray-500">({{ stat.code }})</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% set percent = (stat.translated_count / stat.total_count * 100)|round %}
<div class="flex items-center">
<div class="w-full bg-gray-200 rounded-full h-2.5 mr-2">
<div class="bg-purple-600 h-2.5 rounded-full" style="width: {{ percent }}%"></div>
</div>
<span class="text-sm text-gray-700">{{ percent }}%</span>
</div>
<div class="text-xs text-gray-500 mt-1">
{{ stat.translated_count }} / {{ stat.total_count }} {{ t('settings.keys') }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
{% if stat.code != 'en' and stat.translated_count < stat.total_count %}
<button onclick="translateLanguage('{{ stat.code }}')"
class="text-purple-600 hover:text-purple-900">
<i class="fas fa-robot mr-1"></i>
{{ t('settings.auto_translate') }}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function togglePassword(id) {
const input = document.getElementById(id);
input.type = input.type === 'password' ? 'text' : 'password';
}
function translateLanguage(lang) {
if (!confirm('{{ t('settings.confirm_translate') }}')) return;
const button = event.target.closest('button');
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> {{ t('form.processing') }}';
// Get JWT token from cookie or session
fetch('/api/auth/token', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'email=admin@amnez.ia&password=admin123'
})
.then(r => r.json())
.then(auth => {
return fetch('/api/translations/auto-translate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + auth.token
},
body: JSON.stringify({language: lang})
});
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('{{ t('settings.translation_complete') }}: ' + data.stats.translated + '/' + data.stats.total);
location.reload();
} else {
alert('{{ t('message.error') }}: ' + (data.error || 'Unknown error'));
button.disabled = false;
button.innerHTML = originalText;
}
})
.catch(err => {
alert('{{ t('message.error') }}: ' + err.message);
button.disabled = false;
button.innerHTML = originalText;
});
}
</script>
{% endblock %}