| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- {% extends "layout.html" %}
- {% block content %}
- <div class="max-w-7xl mx-auto p-4 md:p-8">
- <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 border-b border-gray-800 pb-6 gap-4">
- <div>
- <h2 class="text-3xl font-black text-white tracking-tighter italic flex items-center gap-3">
- <svg class="w-8 h-8 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
- Cluster Management
- </h2>
- <p class="text-gray-500 font-bold mt-2 text-[10px] md:text-xs uppercase tracking-widest">Master-Slave DNS Replication</p>
- </div>
- <div class="flex gap-3">
- <button onclick="syncAllZones()" class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-3 rounded-xl font-black uppercase text-[10px] tracking-widest transition shadow-xl shadow-blue-900/20 flex items-center gap-2">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg> Push All Zones
- </button>
- </div>
- </div>
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
- <div class="space-y-8">
-
- <div class="bg-[#12161f] border border-gray-800 rounded-3xl p-6 shadow-2xl">
- <h3 class="text-lg font-black text-white uppercase tracking-tighter mb-4 border-b border-gray-800 pb-3">Master Configuration</h3>
-
- <form action="/cluster" method="POST" class="mb-6">
- <input type="hidden" name="action" value="save_ip">
- <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">This Server's Public IP</label>
- <div class="flex gap-2">
- <input type="text" name="server_public_ip" value="{{ settings.server_public_ip }}" placeholder="e.g. 192.168.1.100" class="flex-1 bg-[#0b0e14] border border-gray-800 rounded-xl px-4 py-2 text-white font-mono text-sm outline-none focus:border-purple-500 transition">
- <button type="submit" class="bg-purple-600 hover:bg-purple-500 text-white px-4 py-2 rounded-xl font-black uppercase text-[10px] transition">Save</button>
- </div>
- <p class="text-[9px] text-gray-600 font-bold mt-2 uppercase">Used in SOA records and to allow Slave transfers.</p>
- </form>
- <div class="bg-gray-900/50 rounded-xl p-4 border border-gray-800">
- <p class="text-[10px] font-black text-gray-500 uppercase mb-2">Master API Key</p>
- <div class="flex items-center justify-between gap-4">
- <code class="text-xs text-green-400 break-all select-all">{{ settings.api_key }}</code>
- <form action="/cluster" method="POST" onsubmit="return confirm('Regenerating API Key will disconnect all current nodes. Continue?');">
- <input type="hidden" name="action" value="regenerate_api">
- <button type="submit" class="text-gray-500 hover:text-white transition" title="Regenerate API Key">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
- </button>
- </form>
- </div>
- </div>
- </div>
- <div class="bg-[#12161f] border border-gray-800 rounded-3xl p-6 shadow-2xl">
- <h3 class="text-lg font-black text-blue-400 uppercase tracking-tighter mb-4 border-b border-gray-800 pb-3">Add Slave Node</h3>
- <form action="/cluster" method="POST" class="space-y-4">
- <input type="hidden" name="action" value="add_node">
- <div>
- <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Node Name</label>
- <input type="text" name="node_name" required placeholder="e.g. Frankfurt Slave" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-4 py-2 text-white text-sm outline-none focus:border-blue-500 transition">
- </div>
- <div>
- <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Panel URL</label>
- <input type="text" name="node_url" required placeholder="http://192.168.1.101:5000" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-4 py-2 text-white font-mono text-sm outline-none focus:border-blue-500 transition">
- </div>
- <div>
- <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Slave's API Key</label>
- <input type="text" name="node_api_key" required placeholder="Paste the API key from the slave panel..." class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-4 py-2 text-green-400 font-mono text-xs outline-none focus:border-blue-500 transition">
- </div>
- <button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 text-white px-4 py-3 rounded-xl font-black uppercase text-[10px] tracking-widest shadow-xl transition mt-2">Connect Node</button>
- </form>
- </div>
-
- </div>
- <div class="lg:col-span-2 space-y-6">
- <h3 class="text-xl font-black text-white uppercase tracking-tighter flex items-center gap-2">
- <svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path></svg>
- Connected Slaves
- </h3>
- {% if settings.cluster_nodes %}
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
- {% for node in settings.cluster_nodes %}
- <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-5 shadow-xl relative group">
- <div class="flex justify-between items-start mb-3">
- <div>
- <h4 class="text-base font-bold text-white flex items-center gap-2">
- {{ node.name }}
- </h4>
- <p class="text-xs text-gray-500 font-mono mt-1">{{ node.url }}</p>
- </div>
- <div class="flex gap-2 items-center">
- <span id="status_{{ loop.index0 }}" class="px-2 py-1 rounded-md text-[9px] font-black uppercase tracking-widest bg-gray-800 text-gray-400">Checking...</span>
- </div>
- </div>
- <div class="flex gap-2 mt-4 pt-4 border-t border-gray-800/50">
- <button onclick="controlNodeService('{{ node.name }}', 'restart')" class="flex-1 bg-gray-800 hover:bg-gray-700 text-gray-300 py-2 rounded-lg text-[9px] font-black uppercase tracking-widest transition flex items-center justify-center gap-1">
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg> Restart
- </button>
- <button onclick="controlNodeService('{{ node.name }}', 'stop')" class="flex-1 bg-red-900/20 hover:bg-red-600 text-red-400 hover:text-white py-2 rounded-lg text-[9px] font-black uppercase tracking-widest transition border border-red-900/30 flex items-center justify-center gap-1">
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path></svg> Stop
- </button>
- <button onclick="controlNodeService('{{ node.name }}', 'start')" class="flex-1 bg-green-900/20 hover:bg-green-600 text-green-400 hover:text-white py-2 rounded-lg text-[9px] font-black uppercase tracking-widest transition border border-green-900/30 flex items-center justify-center gap-1">
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> Start
- </button>
- </div>
- <form action="/cluster" method="POST" class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition" onsubmit="return confirm('Remove this node?');">
- <input type="hidden" name="action" value="delete_node">
- <input type="hidden" name="node_index" value="{{ loop.index0 }}">
- <button type="submit" class="text-red-500 hover:text-red-400 bg-[#12161f] p-1 rounded-md" title="Remove Node">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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"></path></svg>
- </button>
- </form>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <div class="text-center p-12 border-2 border-dashed border-gray-800 rounded-3xl">
- <svg class="w-12 h-12 text-gray-600 mx-auto mb-4" 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"></path></svg>
- <h4 class="text-lg font-bold text-gray-400 uppercase tracking-widest mb-1">No Slaves Connected</h4>
- <p class="text-xs text-gray-600">Add a node from the left panel to begin replication.</p>
- </div>
- {% endif %}
- </div>
- </div>
- </div>
- <script>
- // AJAX Function: Ping nodes and check status
- function checkNodes() {
- const nodes = [
- {% for node in settings.cluster_nodes %}
- {name: "{{ node.name }}", idx: {{ loop.index0 }}},
- {% endfor %}
- ];
- nodes.forEach(node => {
- const badge = document.getElementById(`status_${node.idx}`);
- fetch('/cluster/check_node', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({node_name: node.name})
- })
- .then(res => res.json())
- .then(data => {
- if(data.status === 'online') {
- badge.className = 'px-2 py-1 rounded-md text-[9px] font-black uppercase tracking-widest bg-green-500/20 text-green-400 border border-green-500/30';
- badge.innerText = 'Online';
- } else if(data.status === 'auth_error') {
- badge.className = 'px-2 py-1 rounded-md text-[9px] font-black uppercase tracking-widest bg-yellow-500/20 text-yellow-500 border border-yellow-500/30';
- badge.innerText = 'Auth Error';
- } else {
- badge.className = 'px-2 py-1 rounded-md text-[9px] font-black uppercase tracking-widest bg-red-500/20 text-red-500 border border-red-500/30';
- badge.innerText = 'Offline';
- }
- })
- .catch(() => {
- badge.className = 'px-2 py-1 rounded-md text-[9px] font-black uppercase tracking-widest bg-red-500/20 text-red-500 border border-red-500/30';
- badge.innerText = 'Error';
- });
- });
- }
- // AJAX Function: Service Control (Start/Stop/Restart)
- function controlNodeService(nodeName, action) {
- if(!confirm(`Are you sure you want to ${action.toUpperCase()} the BIND service on ${nodeName}?`)) return;
-
- AppToast.fire({ icon: 'info', title: `Sending ${action} command...` });
- fetch('/cluster/node_service', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({node_name: nodeName, action: action})
- })
- .then(res => res.json())
- .then(data => {
- if(data.status === 'success') {
- AppToast.fire({ icon: 'success', title: 'Success', text: data.message });
- } else {
- AppToast.fire({ icon: 'error', title: 'Action Failed', text: data.message });
- }
- })
- .catch(err => {
- AppToast.fire({ icon: 'error', title: 'Network Error', text: 'Could not reach the node.' });
- });
- }
- // AJAX Function: Push All Zones
- function syncAllZones() {
- if(!confirm("Are you sure you want to push ALL Master zones to ALL connected Slave nodes? This might take a few moments.")) return;
-
- AppToast.fire({ icon: 'info', title: `Initiating Global Sync...` });
- fetch('/api/cluster/sync_all', { method: 'POST' })
- .then(res => res.json())
- .then(data => {
- if(data.status === 'success') {
- AppToast.fire({ icon: 'success', title: 'Sync Started', text: data.message });
- } else {
- AppToast.fire({ icon: 'error', title: 'Sync Failed', text: data.message });
- }
- })
- .catch(err => {
- AppToast.fire({ icon: 'error', title: 'Error', text: 'Failed to initiate sync.' });
- });
- }
- // Check nodes on load and every 10 seconds
- document.addEventListener('DOMContentLoaded', () => {
- checkNodes();
- setInterval(checkNodes, 10000);
- });
- </script>
- {% endblock %}
|