cluster.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. {% extends "layout.html" %}
  2. {% block content %}
  3. <div class="max-w-7xl mx-auto p-4 md:p-8">
  4. <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">
  5. <div>
  6. <h2 class="text-3xl font-black text-white tracking-tighter italic flex items-center gap-3">
  7. <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>
  8. Cluster Management
  9. </h2>
  10. <p class="text-gray-500 font-bold mt-2 text-[10px] md:text-xs uppercase tracking-widest">Master-Slave DNS Replication</p>
  11. </div>
  12. <div class="flex gap-3">
  13. <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">
  14. <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
  15. </button>
  16. </div>
  17. </div>
  18. <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
  19. <div class="space-y-8">
  20. <div class="bg-[#12161f] border border-gray-800 rounded-3xl p-6 shadow-2xl">
  21. <h3 class="text-lg font-black text-white uppercase tracking-tighter mb-4 border-b border-gray-800 pb-3">Master Configuration</h3>
  22. <form action="/cluster" method="POST" class="mb-6">
  23. <input type="hidden" name="action" value="save_ip">
  24. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">This Server's Public IP</label>
  25. <div class="flex gap-2">
  26. <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">
  27. <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>
  28. </div>
  29. <p class="text-[9px] text-gray-600 font-bold mt-2 uppercase">Used in SOA records and to allow Slave transfers.</p>
  30. </form>
  31. <div class="bg-gray-900/50 rounded-xl p-4 border border-gray-800">
  32. <p class="text-[10px] font-black text-gray-500 uppercase mb-2">Master API Key</p>
  33. <div class="flex items-center justify-between gap-4">
  34. <code class="text-xs text-green-400 break-all select-all">{{ settings.api_key }}</code>
  35. <form action="/cluster" method="POST" onsubmit="return confirm('Regenerating API Key will disconnect all current nodes. Continue?');">
  36. <input type="hidden" name="action" value="regenerate_api">
  37. <button type="submit" class="text-gray-500 hover:text-white transition" title="Regenerate API Key">
  38. <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>
  39. </button>
  40. </form>
  41. </div>
  42. </div>
  43. </div>
  44. <div class="bg-[#12161f] border border-gray-800 rounded-3xl p-6 shadow-2xl">
  45. <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>
  46. <form action="/cluster" method="POST" class="space-y-4">
  47. <input type="hidden" name="action" value="add_node">
  48. <div>
  49. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Node Name</label>
  50. <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">
  51. </div>
  52. <div>
  53. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Panel URL</label>
  54. <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">
  55. </div>
  56. <div>
  57. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Slave's API Key</label>
  58. <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">
  59. </div>
  60. <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>
  61. </form>
  62. </div>
  63. </div>
  64. <div class="lg:col-span-2 space-y-6">
  65. <h3 class="text-xl font-black text-white uppercase tracking-tighter flex items-center gap-2">
  66. <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>
  67. Connected Slaves
  68. </h3>
  69. {% if settings.cluster_nodes %}
  70. <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  71. {% for node in settings.cluster_nodes %}
  72. <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-5 shadow-xl relative group">
  73. <div class="flex justify-between items-start mb-3">
  74. <div>
  75. <h4 class="text-base font-bold text-white flex items-center gap-2">
  76. {{ node.name }}
  77. </h4>
  78. <p class="text-xs text-gray-500 font-mono mt-1">{{ node.url }}</p>
  79. </div>
  80. <div class="flex gap-2 items-center">
  81. <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>
  82. </div>
  83. </div>
  84. <div class="flex gap-2 mt-4 pt-4 border-t border-gray-800/50">
  85. <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">
  86. <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
  87. </button>
  88. <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">
  89. <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
  90. </button>
  91. <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">
  92. <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
  93. </button>
  94. </div>
  95. <form action="/cluster" method="POST" class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition" onsubmit="return confirm('Remove this node?');">
  96. <input type="hidden" name="action" value="delete_node">
  97. <input type="hidden" name="node_index" value="{{ loop.index0 }}">
  98. <button type="submit" class="text-red-500 hover:text-red-400 bg-[#12161f] p-1 rounded-md" title="Remove Node">
  99. <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>
  100. </button>
  101. </form>
  102. </div>
  103. {% endfor %}
  104. </div>
  105. {% else %}
  106. <div class="text-center p-12 border-2 border-dashed border-gray-800 rounded-3xl">
  107. <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>
  108. <h4 class="text-lg font-bold text-gray-400 uppercase tracking-widest mb-1">No Slaves Connected</h4>
  109. <p class="text-xs text-gray-600">Add a node from the left panel to begin replication.</p>
  110. </div>
  111. {% endif %}
  112. </div>
  113. </div>
  114. </div>
  115. <script>
  116. // AJAX Function: Ping nodes and check status
  117. function checkNodes() {
  118. const nodes = [
  119. {% for node in settings.cluster_nodes %}
  120. {name: "{{ node.name }}", idx: {{ loop.index0 }}},
  121. {% endfor %}
  122. ];
  123. nodes.forEach(node => {
  124. const badge = document.getElementById(`status_${node.idx}`);
  125. fetch('/cluster/check_node', {
  126. method: 'POST',
  127. headers: {'Content-Type': 'application/json'},
  128. body: JSON.stringify({node_name: node.name})
  129. })
  130. .then(res => res.json())
  131. .then(data => {
  132. if(data.status === 'online') {
  133. 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';
  134. badge.innerText = 'Online';
  135. } else if(data.status === 'auth_error') {
  136. 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';
  137. badge.innerText = 'Auth Error';
  138. } else {
  139. 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';
  140. badge.innerText = 'Offline';
  141. }
  142. })
  143. .catch(() => {
  144. 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';
  145. badge.innerText = 'Error';
  146. });
  147. });
  148. }
  149. // AJAX Function: Service Control (Start/Stop/Restart)
  150. function controlNodeService(nodeName, action) {
  151. if(!confirm(`Are you sure you want to ${action.toUpperCase()} the BIND service on ${nodeName}?`)) return;
  152. AppToast.fire({ icon: 'info', title: `Sending ${action} command...` });
  153. fetch('/cluster/node_service', {
  154. method: 'POST',
  155. headers: {'Content-Type': 'application/json'},
  156. body: JSON.stringify({node_name: nodeName, action: action})
  157. })
  158. .then(res => res.json())
  159. .then(data => {
  160. if(data.status === 'success') {
  161. AppToast.fire({ icon: 'success', title: 'Success', text: data.message });
  162. } else {
  163. AppToast.fire({ icon: 'error', title: 'Action Failed', text: data.message });
  164. }
  165. })
  166. .catch(err => {
  167. AppToast.fire({ icon: 'error', title: 'Network Error', text: 'Could not reach the node.' });
  168. });
  169. }
  170. // AJAX Function: Push All Zones
  171. function syncAllZones() {
  172. if(!confirm("Are you sure you want to push ALL Master zones to ALL connected Slave nodes? This might take a few moments.")) return;
  173. AppToast.fire({ icon: 'info', title: `Initiating Global Sync...` });
  174. fetch('/api/cluster/sync_all', { method: 'POST' })
  175. .then(res => res.json())
  176. .then(data => {
  177. if(data.status === 'success') {
  178. AppToast.fire({ icon: 'success', title: 'Sync Started', text: data.message });
  179. } else {
  180. AppToast.fire({ icon: 'error', title: 'Sync Failed', text: data.message });
  181. }
  182. })
  183. .catch(err => {
  184. AppToast.fire({ icon: 'error', title: 'Error', text: 'Failed to initiate sync.' });
  185. });
  186. }
  187. // Check nodes on load and every 10 seconds
  188. document.addEventListener('DOMContentLoaded', () => {
  189. checkNodes();
  190. setInterval(checkNodes, 10000);
  191. });
  192. </script>
  193. {% endblock %}