dashboard.html 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. {% extends "layout.html" %}
  2. {% block content %}
  3. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  4. <div class="flex justify-between items-center mb-6">
  5. <div>
  6. <h2 class="text-4xl font-black text-white tracking-tighter italic">DNS Zones Dashboard</h2>
  7. {% if session.get('role') == 'admin' %}
  8. <div class="flex items-center gap-4 mt-2">
  9. <div class="flex items-center gap-2">
  10. <span id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-600 transition-all duration-500"></span>
  11. <span id="status-text" class="text-[10px] font-bold text-gray-500 uppercase tracking-widest leading-none">Checking Service...</span>
  12. </div>
  13. <div class="flex gap-2 border-l border-gray-800 pl-4">
  14. <a href="/service/start" class="text-[9px] bg-green-600/20 text-green-400 px-3 py-1 rounded font-black border border-green-500/30">Start</a>
  15. <a href="/service/stop" class="text-[9px] bg-red-600/20 text-red-400 px-3 py-1 rounded font-black border border-red-500/30">Stop</a>
  16. <a href="/service/restart" class="text-[9px] bg-yellow-600/20 text-yellow-400 hover:bg-yellow-600/40 px-3 py-1 rounded font-black border border-yellow-500/30">Restart</a>
  17. <a href="/service/reload" title="Reload" class="text-[9px] bg-blue-600/20 text-blue-400 px-3 py-1 rounded font-black border border-blue-500/30">Apply</a>
  18. </div>
  19. </div>
  20. {% endif %}
  21. </div>
  22. <div class="flex gap-3">
  23. {% if session.get('role') == 'admin' %}
  24. <button onclick="openBackupModal()" class="bg-gray-800 hover:bg-gray-700 border border-gray-700 px-6 py-3 rounded-xl font-black shadow-lg transition text-white uppercase text-xs tracking-widest flex items-center gap-2">
  25. <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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>Backup
  26. </button>
  27. {% endif %}
  28. <button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-500 px-8 py-3 rounded-xl font-black shadow-2xl transition transform hover:scale-105 text-white uppercase text-xs tracking-widest">+ New Zone</button>
  29. </div>
  30. </div>
  31. {% if session.get('role') == 'admin' %}
  32. <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-10">
  33. <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl relative">
  34. <div class="flex justify-between items-center mb-4 text-[10px] font-black text-gray-500 uppercase tracking-widest">
  35. <h3>Requests Per Second</h3>
  36. <span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span></span>
  37. </div>
  38. <div class="h-40"><canvas id="rpsChart"></canvas></div>
  39. </div>
  40. <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl relative">
  41. <div class="flex justify-between items-center mb-4 text-[10px] font-black text-gray-500 uppercase tracking-widest">
  42. <h3>Top TLDs</h3>
  43. <span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span></span>
  44. </div>
  45. <div class="h-40 flex justify-center"><canvas id="tldChart"></canvas></div>
  46. </div>
  47. <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl relative">
  48. <div class="flex justify-between items-center mb-4 text-[10px] font-black text-gray-500 uppercase tracking-widest">
  49. <h3>Unique IPs</h3>
  50. <span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-purple-400 opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-purple-500"></span></span>
  51. </div>
  52. <div class="h-40"><canvas id="ipsChart"></canvas></div>
  53. </div>
  54. </div>
  55. {% endif %}
  56. <div class="flex border-b border-gray-800 mb-6 gap-6 overflow-x-auto whitespace-nowrap scrollbar-hide">
  57. <button onclick="switchTab('master_forward')" id="btnTab_master_forward" class="pb-4 text-[10px] font-black uppercase tracking-widest text-blue-500 border-b-2 border-blue-500 transition focus:outline-none">Master Forward</button>
  58. <button onclick="switchTab('master_reverse')" id="btnTab_master_reverse" class="pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none">Master Reverse</button>
  59. <button onclick="switchTab('slave_forward')" id="btnTab_slave_forward" class="pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none">Slave Forward</button>
  60. <button onclick="switchTab('slave_reverse')" id="btnTab_slave_reverse" class="pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none">Slave Reverse</button>
  61. <button onclick="switchTab('forward_zones')" id="btnTab_forward_zones" class="pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none">Forward Zones</button>
  62. </div>
  63. <div class="flex flex-col md:flex-row gap-4 mb-8">
  64. <input type="text" id="zoneSearch" onkeyup="filterZones()" placeholder="Search zones..." class="flex-1 bg-[#12161f] border border-gray-800 rounded-2xl py-4 px-6 text-white font-mono outline-none focus:border-blue-500 transition text-sm">
  65. <div class="flex bg-[#12161f] border border-gray-800 rounded-2xl p-1 h-[54px]">
  66. <button onclick="changeViewMode('cards')" id="btnViewCards" class="px-6 rounded-xl text-xs font-black uppercase transition {% if view_mode != 'table' %}bg-blue-600 text-white shadow-lg{% else %}text-gray-500{% endif %}">Cards</button>
  67. <button onclick="changeViewMode('table')" id="btnViewTable" class="px-6 rounded-xl text-xs font-black uppercase transition {% if view_mode == 'table' %}bg-blue-600 text-white shadow-lg{% else %}text-gray-500{% endif %}">Table</button>
  68. </div>
  69. </div>
  70. <div id="dashboard-dynamic-content">
  71. <div class="{% if view_mode == 'table' %}hidden{% endif %} grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6" id="zonesGrid">
  72. {% for zone_name, ztype in zones %}
  73. {% set url_param = 'root_zone' if zone_name == '.' else zone_name %}
  74. {% set is_reverse = '.arpa' in zone_name %}
  75. {% set is_master = ztype == 'master' or ztype == 'hint' %}
  76. {% set is_hint = ztype == 'hint' %}
  77. {% set is_forward = ztype == 'forward' %}
  78. {% set tab_cat = 'forward_zones' if is_forward else ('master_forward' if is_master and not is_reverse else 'master_reverse' if is_master and is_reverse else 'slave_forward' if not is_master and not is_reverse else 'slave_reverse') %}
  79. <div class="zone-item zone-card bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl hover:border-blue-500/40 transition group relative" data-domain="{{ zone_name|lower }}" data-owner="{{ owners.get(zone_name, 'admin')|lower }}" data-category="{{ tab_cat }}">
  80. <div class="flex justify-between items-start mb-4">
  81. <div class="flex gap-2 flex-wrap">
  82. <span class="text-[9px] font-black uppercase tracking-widest px-2 py-1 rounded bg-gray-900 {% if is_hint %}text-yellow-400 border border-yellow-500/20{% elif ztype=='master' %}text-green-400 border border-green-500/20{% elif ztype=='forward' %}text-cyan-400 border border-cyan-500/20{% else %}text-blue-400 border border-blue-500/20{% endif %}">{{ ztype }}</span>
  83. {% if session.get('role') == 'admin' %}
  84. <span class="text-[9px] font-black text-gray-400 bg-gray-800/50 px-2 py-1 rounded">{{ owners.get(zone_name, 'admin') }}</span>
  85. {% endif %}
  86. </div>
  87. <div class="flex gap-3 opacity-0 group-hover:opacity-100 transition items-center">
  88. {% if session.get('role') == 'admin' %}
  89. <button type="button" onclick="openAssignModal('{{ zone_name|e }}', '{{ owners.get(zone_name, 'admin')|e }}')" class="text-blue-400 text-[10px] font-black uppercase tracking-widest hover:text-blue-300">Assign</button>
  90. {% endif %}
  91. <a href="/zones/delete/{{ url_param|urlencode }}" onclick="handleAjaxLink(event, this, 'dashboard-dynamic-content', restoreDashboardState, 'Delete this zone permanently?')" class="text-red-500 text-[10px] font-black uppercase tracking-widest hover:text-red-400 transition">Delete</a>
  92. </div>
  93. </div>
  94. <h3 class="text-xl font-bold text-white mb-6 font-mono truncate" title="{{ zone_name }}">
  95. {% if zone_name == '.' %}. (Root Zone){% else %}{{ zone_name }}{% endif %}
  96. </h3>
  97. <a href="/zones/manage/{{ url_param|urlencode }}" class="block w-full text-center bg-gray-800 hover:bg-blue-600 py-3 rounded-xl text-gray-300 hover:text-white font-black text-xs transition uppercase tracking-widest">Manage</a>
  98. </div>
  99. {% endfor %}
  100. </div>
  101. <div id="zonesTableContainer" class="{% if view_mode != 'table' %}hidden{% endif %} bg-[#12161f] border border-gray-800 rounded-3xl overflow-x-auto shadow-2xl mb-8">
  102. <table class="w-full text-left whitespace-nowrap">
  103. <thead class="bg-black/20 text-[10px] font-black text-gray-500 uppercase border-b border-gray-800">
  104. <tr><th class="p-6">Domain / IP Subnet</th><th class="p-6">Type</th><th class="p-6">Owner</th><th class="p-6 text-right">Actions</th></tr>
  105. </thead>
  106. <tbody class="divide-y divide-gray-800">
  107. {% for zone_name, ztype in zones %}
  108. {% set url_param = 'root_zone' if zone_name == '.' else zone_name %}
  109. {% set is_reverse = '.arpa' in zone_name %}
  110. {% set is_master = ztype == 'master' or ztype == 'hint' %}
  111. {% set is_forward = ztype == 'forward' %}
  112. {% set tab_cat = 'forward_zones' if is_forward else ('master_forward' if is_master and not is_reverse else 'master_reverse' if is_master and is_reverse else 'slave_forward' if not is_master and not is_reverse else 'slave_reverse') %}
  113. <tr class="zone-item group" data-domain="{{ zone_name|lower }}" data-owner="{{ owners.get(zone_name, 'admin')|lower }}" data-category="{{ tab_cat }}">
  114. <td class="p-6 font-mono font-bold text-white">{% if zone_name == '.' %}. (Root Zone){% else %}{{ zone_name }}{% endif %}</td>
  115. <td class="p-6"><span class="text-[9px] font-black uppercase px-2 py-1 rounded bg-gray-900 {% if is_forward %}text-cyan-400{% else %}text-blue-400{% endif %}">{{ ztype }}</span></td>
  116. <td class="p-6 text-gray-400 text-xs uppercase">{{ owners.get(zone_name, 'admin') }}</td>
  117. <td class="p-6 text-right">
  118. <div class="flex justify-end gap-4 items-center">
  119. {% if session.get('role') == 'admin' %}
  120. <button type="button" onclick="openAssignModal('{{ zone_name|e }}', '{{ owners.get(zone_name, 'admin')|e }}')" class="opacity-0 group-hover:opacity-100 text-purple-400 text-[10px] font-black uppercase transition">Assign</button>
  121. {% endif %}
  122. <a href="/zones/manage/{{ url_param|urlencode }}" class="text-green-400 font-black text-[10px] uppercase">Manage</a>
  123. <a href="/zones/delete/{{ url_param|urlencode }}" onclick="handleAjaxLink(event, this, 'dashboard-dynamic-content', restoreDashboardState, 'Delete this zone permanently?')" class="opacity-0 group-hover:opacity-100 text-red-500 text-[10px] font-black uppercase transition">Delete</a>
  124. </div>
  125. </td>
  126. </tr>
  127. {% endfor %}
  128. </tbody>
  129. </table>
  130. </div>
  131. <div id="noZonesMsg" class="hidden py-20 text-center border-2 border-dashed border-gray-800 rounded-3xl mt-6"><span class="text-gray-600 font-black uppercase text-xs">No zones found in this category</span></div>
  132. </div>
  133. <div id="assignZoneModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
  134. <div class="bg-[#12161f] border border-gray-800 w-full max-w-sm rounded-3xl p-8 shadow-2xl">
  135. <h3 class="text-xl font-black text-white mb-6 uppercase text-center">Assign Ownership</h3>
  136. <form action="/zones/assign" method="POST" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'dashboard-dynamic-content', restoreDashboardState)">
  137. <input type="hidden" name="domain" id="assignDomainInput">
  138. <div class="text-center mb-4"><span id="assignDomainDisplay" class="text-lg font-mono text-blue-400 font-bold"></span></div>
  139. <select name="owner" id="assignOwnerSelect" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none cursor-pointer">
  140. <option value="admin">admin (Unassign)</option>
  141. {% for u in all_usernames %}
  142. {% if u != 'admin' %}<option value="{{ u }}">{{ u }}</option>{% endif %}
  143. {% endfor %}
  144. </select>
  145. <div class="flex gap-4 pt-6">
  146. <button type="button" onclick="closeAssignModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-4 rounded-xl uppercase text-xs">Cancel</button>
  147. <button type="submit" class="flex-1 bg-blue-600 text-white font-black py-4 rounded-xl uppercase text-xs shadow-xl">Assign</button>
  148. </div>
  149. </form>
  150. </div>
  151. </div>
  152. <div id="addZoneModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
  153. <div class="bg-[#12161f] border border-gray-800 w-full max-w-lg rounded-3xl p-8 shadow-2xl">
  154. <h3 class="text-2xl font-black text-white mb-8 uppercase border-b border-gray-800 pb-4 text-center">Create New Zone</h3>
  155. <form action="/zones/add" method="POST" id="addZoneForm" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'dashboard-dynamic-content', restoreDashboardState)">
  156. <div class="grid grid-cols-2 gap-4">
  157. <div>
  158. <label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Direction</label>
  159. <select name="direction" id="zoneDirection" onchange="updatePlaceholder()" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none text-sm cursor-pointer">
  160. <option value="forward">Forward</option>
  161. <option value="reverse">Reverse</option>
  162. </select>
  163. </div>
  164. <div>
  165. <label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Type</label>
  166. <select name="type" id="zoneType" onchange="toggleZoneFields()" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none text-sm cursor-pointer">
  167. <option value="master">Master</option>
  168. <option value="slave">Slave</option>
  169. <option value="forward">Forwarder</option>
  170. <option value="hint">Hint (Root Zone)</option>
  171. </select>
  172. </div>
  173. </div>
  174. <div id="domainContainer">
  175. <label id="domainLabel" class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Domain Name</label>
  176. <input type="text" name="domain" id="domainInput" placeholder="example.com" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-mono outline-none focus:border-blue-500 transition-colors">
  177. </div>
  178. <div id="masterFields" class="space-y-4">
  179. <label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Template (Required)</label>
  180. <select name="template_name" id="templateSelect" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none cursor-pointer" required>
  181. {% if not templates %}
  182. <option value="" disabled selected>No templates assigned to your account!</option>
  183. {% else %}
  184. {% for tname in templates.keys() %}
  185. <option value="{{ tname }}">{{ tname }}</option>
  186. {% endfor %}
  187. {% endif %}
  188. </select>
  189. </div>
  190. <div id="slaveFields" class="hidden space-y-4">
  191. <div class="flex justify-between items-center">
  192. <label class="text-[10px] font-bold text-gray-500 uppercase">Master IPs</label>
  193. <button type="button" onclick="addDynamicIp('slaveIpCont', 'master_ips[]')" class="text-[9px] bg-purple-600/20 text-purple-400 px-3 py-1 rounded font-black uppercase">+ Add</button>
  194. </div>
  195. <div id="slaveIpCont" class="space-y-2">
  196. <input type="text" name="master_ips[]" placeholder="1.2.3.4" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">
  197. </div>
  198. </div>
  199. <div id="forwardFields" class="hidden space-y-4">
  200. <div class="flex justify-between items-center">
  201. <label class="text-[10px] font-bold text-gray-500 uppercase">Forwarders</label>
  202. <button type="button" onclick="addDynamicIp('forwardIpCont', 'forwarders[]')" class="text-[9px] bg-blue-600/20 text-blue-400 px-3 py-1 rounded font-black uppercase">+ Add</button>
  203. </div>
  204. <div id="forwardIpCont" class="space-y-2">
  205. <input type="text" name="forwarders[]" placeholder="8.8.8.8" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">
  206. </div>
  207. </div>
  208. <div class="flex gap-4 pt-6 border-t border-gray-800">
  209. <button type="button" onclick="closeAddModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-4 rounded-xl hover:text-white transition uppercase text-xs tracking-widest">Cancel</button>
  210. <button type="submit" class="flex-1 bg-blue-600 text-white font-black py-4 rounded-xl shadow-xl transition uppercase text-xs tracking-widest hover:bg-blue-500">Create</button>
  211. </div>
  212. </form>
  213. </div>
  214. </div>
  215. <div id="backupModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
  216. <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
  217. <h3 class="text-2xl font-black text-white mb-8 uppercase text-center italic">System Backup & Restore</h3>
  218. <div class="space-y-6">
  219. <div class="bg-gray-900/50 p-6 rounded-2xl border border-gray-800">
  220. <h4 class="text-xs font-black text-green-500 uppercase mb-2">Export Data</h4>
  221. <a href="/backup/export" class="block w-full text-center bg-green-600/20 text-green-400 font-black py-3 rounded-xl border border-green-500/30 uppercase text-xs">Download ZIP</a>
  222. </div>
  223. <div class="bg-gray-900/50 p-6 rounded-2xl border border-gray-800">
  224. <h4 class="text-xs font-black text-blue-500 uppercase mb-2">Restore Data</h4>
  225. <form action="/backup/import" method="POST" enctype="multipart/form-data" class="space-y-4">
  226. <input type="file" name="backup_file" required class="block w-full text-xs text-gray-400 file:bg-gray-800 file:text-white file:border-0 file:py-2 file:px-4 file:rounded-lg">
  227. <button type="submit" class="w-full bg-blue-600 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl">Restore ZIP</button>
  228. </form>
  229. </div>
  230. </div>
  231. <button onclick="closeBackupModal()" class="w-full mt-6 bg-gray-800 text-gray-400 py-3 rounded-xl uppercase text-xs font-bold">Close</button>
  232. </div>
  233. </div>
  234. <script>
  235. // CHART LOGIC
  236. let rpsChartInst = null, ipsChartInst = null, tldChartInst = null;
  237. function renderLiveCharts() {
  238. fetch('/api/stats').then(r => r.json()).then(data => {
  239. const today = new Date().toISOString().split('T')[0];
  240. const todayData = data[today] || { timeline: {}, tlds: {} };
  241. const times = Object.keys(todayData.timeline).sort();
  242. const rpsData = times.map(t => todayData.timeline[t].rps);
  243. const ipsData = times.map(t => todayData.timeline[t].unique_ips);
  244. const tldLabels = Object.keys(todayData.tlds).sort((a,b) => todayData.tlds[b] - todayData.tlds[a]).slice(0, 5);
  245. const tldData = tldLabels.map(t => todayData.tlds[t]);
  246. const chartOptions = { animation: false, interaction: { mode: 'index', intersect: false }, responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { grid: { color: '#1f2937' }, beginAtZero: true } } };
  247. if(!rpsChartInst) {
  248. rpsChartInst = new Chart(document.getElementById('rpsChart'), { type: 'line', data: { labels: times, datasets: [{ data: rpsData, borderColor: '#3b82f6', fill: true, tension: 0.4 }] }, options: chartOptions });
  249. } else {
  250. rpsChartInst.data.labels = times; rpsChartInst.data.datasets[0].data = rpsData; rpsChartInst.update('none');
  251. }
  252. if(!ipsChartInst) {
  253. ipsChartInst = new Chart(document.getElementById('ipsChart'), { type: 'bar', data: { labels: times, datasets: [{ data: ipsData, backgroundColor: '#8b5cf6' }] }, options: chartOptions });
  254. } else {
  255. ipsChartInst.data.labels = times; ipsChartInst.data.datasets[0].data = ipsData; ipsChartInst.update('none');
  256. }
  257. if(!tldChartInst) {
  258. tldChartInst = new Chart(document.getElementById('tldChart'), { type: 'doughnut', data: { labels: tldLabels, datasets: [{ data: tldData, backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#6366f1'], borderWidth: 0 }] }, options: { animation: false, responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#9ca3af', font: { size: 10 } } } }, cutout: '70%' } });
  259. } else {
  260. tldChartInst.data.labels = tldLabels; tldChartInst.data.datasets[0].data = tldData; tldChartInst.update('none');
  261. }
  262. }).catch(e => console.log('Stats sync error:', e));
  263. }
  264. // AJAX HELPERS
  265. async function handleAjaxForm(e, form, containerId, callback) {
  266. e.preventDefault();
  267. const btn = form.querySelector('button[type="submit"]');
  268. if(btn) { btn.disabled = true; }
  269. try {
  270. const res = await fetch(form.action, { method: form.method || 'POST', body: new FormData(form) });
  271. const html = await res.text();
  272. const doc = new DOMParser().parseFromString(html, 'text/html');
  273. const newCont = doc.getElementById(containerId);
  274. if(newCont) {
  275. document.getElementById(containerId).innerHTML = newCont.innerHTML;
  276. if(callback) callback();
  277. AppToast.fire({ icon: 'success', title: 'Success' });
  278. } else window.location.reload();
  279. } catch(err) { window.location.reload(); }
  280. }
  281. async function handleAjaxLink(e, link, containerId, callback, confirmMsg) {
  282. e.preventDefault();
  283. if(confirmMsg && !confirm(confirmMsg)) return;
  284. try {
  285. const res = await fetch(link.href);
  286. const html = await res.text();
  287. const doc = new DOMParser().parseFromString(html, 'text/html');
  288. const newCont = doc.getElementById(containerId);
  289. if(newCont) {
  290. document.getElementById(containerId).innerHTML = newCont.innerHTML;
  291. if(callback) callback();
  292. AppToast.fire({ icon: 'success', title: 'Action Completed' });
  293. } else window.location.reload();
  294. } catch(err) { window.location.reload(); }
  295. }
  296. // DASHBOARD LOGIC
  297. function openBackupModal() { document.getElementById('backupModal').classList.remove('hidden'); }
  298. function closeBackupModal() { document.getElementById('backupModal').classList.add('hidden'); }
  299. function openAddModal() {
  300. document.getElementById('addZoneForm').reset();
  301. document.getElementById('addZoneModal').classList.remove('hidden');
  302. toggleZoneFields();
  303. }
  304. function closeAddModal() {
  305. // Καθαρισμός όλων των δυναμικών IPs κατά το κλείσιμο
  306. document.getElementById('slaveIpCont').innerHTML = '<input type="text" name="master_ips[]" placeholder="1.2.3.4" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">';
  307. document.getElementById('forwardIpCont').innerHTML = '<input type="text" name="forwarders[]" placeholder="8.8.8.8" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">';
  308. document.getElementById('addZoneModal').classList.add('hidden');
  309. }
  310. function openAssignModal(d, o) {
  311. document.getElementById('assignDomainInput').value=d;
  312. document.getElementById('assignDomainDisplay').innerText=d;
  313. document.getElementById('assignOwnerSelect').value=o;
  314. document.getElementById('assignZoneModal').classList.remove('hidden');
  315. }
  316. function closeAssignModal() {
  317. document.getElementById('assignZoneModal').classList.add('hidden');
  318. }
  319. function toggleZoneFields() {
  320. const type = document.getElementById('zoneType').value;
  321. const domCont = document.getElementById('domainContainer');
  322. const domInp = document.getElementById('domainInput');
  323. const tmplSelect = document.getElementById('templateSelect');
  324. if(type === 'hint') {
  325. domCont.classList.add('hidden');
  326. domInp.value = '.';
  327. } else {
  328. domCont.classList.remove('hidden');
  329. if(domInp.value === '.') domInp.value = '';
  330. }
  331. // Ασφαλής έλεγχος με το νέο ID
  332. if (type === 'master') {
  333. document.getElementById('masterFields').style.display = 'block';
  334. if (tmplSelect) tmplSelect.required = true;
  335. } else {
  336. document.getElementById('masterFields').style.display = 'none';
  337. if (tmplSelect) tmplSelect.required = false;
  338. }
  339. document.getElementById('slaveFields').style.display = (type === 'slave') ? 'block' : 'none';
  340. document.getElementById('forwardFields').style.display = (type === 'forward') ? 'block' : 'none';
  341. }
  342. function updatePlaceholder() {
  343. const dir = document.getElementById('zoneDirection').value, inp = document.getElementById('domainInput'), lbl = document.getElementById('domainLabel');
  344. if(dir === 'reverse') { lbl.innerText = 'IP SUBNET (e.g. 192.168.1)'; inp.placeholder = '192.168.1'; }
  345. else { lbl.innerText = 'DOMAIN NAME'; inp.placeholder = 'example.com'; }
  346. }
  347. function addDynamicIp(contId, name) {
  348. const div = document.createElement('div'); div.className = 'flex gap-2 mt-2';
  349. div.innerHTML = `<input type="text" name="${name}" placeholder="0.0.0.0" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono"><button type="button" onclick="this.parentElement.remove()" class="text-red-500 font-black px-4 hover:text-red-400 transition">×</button>`;
  350. document.getElementById(contId).appendChild(div);
  351. }
  352. function changeViewMode(mode) {
  353. document.getElementById('zonesGrid').classList.toggle('hidden', mode==='table');
  354. document.getElementById('zonesTableContainer').classList.toggle('hidden', mode==='cards');
  355. document.getElementById('btnViewCards').className = mode==='cards' ? 'px-6 rounded-xl text-xs font-black uppercase transition bg-blue-600 text-white shadow-lg' : 'px-6 rounded-xl text-xs font-black uppercase transition text-gray-500';
  356. document.getElementById('btnViewTable').className = mode==='table' ? 'px-6 rounded-xl text-xs font-black uppercase transition bg-blue-600 text-white shadow-lg' : 'px-6 rounded-xl text-xs font-black uppercase transition text-gray-500';
  357. fetch('/settings/view_mode', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({mode: mode}) });
  358. }
  359. let activeTab = 'master_forward';
  360. function switchTab(tab) {
  361. activeTab = tab;
  362. ['master_forward', 'master_reverse', 'slave_forward', 'slave_reverse', 'forward_zones'].forEach(t => {
  363. const btn = document.getElementById('btnTab_' + t);
  364. if(btn) btn.className = (t === activeTab) ? "pb-4 text-[10px] font-black uppercase tracking-widest text-blue-500 border-b-2 border-blue-500 transition focus:outline-none" : "pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none";
  365. });
  366. filterZones();
  367. }
  368. function filterZones() {
  369. let input = document.getElementById('zoneSearch').value.toLowerCase();
  370. let items = document.querySelectorAll('.zone-item');
  371. let count = 0;
  372. items.forEach(item => {
  373. let domain = item.getAttribute('data-domain') || '';
  374. let owner = item.getAttribute('data-owner') || '';
  375. let category = item.getAttribute('data-category');
  376. let match = (domain.includes(input) || owner.includes(input)) && category === activeTab;
  377. item.classList.toggle('hidden', !match);
  378. if(match) count++;
  379. });
  380. const msg = document.getElementById('noZonesMsg');
  381. if(msg) msg.classList.toggle('hidden', count > 0);
  382. }
  383. function updateLiveStatus() {
  384. fetch('/api/service/status').then(r => r.json()).then(data => {
  385. const dot = document.getElementById('status-dot'), text = document.getElementById('status-text');
  386. if(dot && text) {
  387. if(data.status === 'active') {
  388. dot.className = "w-2.5 h-2.5 rounded-full bg-green-500 shadow-[0_0_10px_rgba(34,197,94,0.8)] animate-pulse";
  389. text.innerHTML = `Service: Active <span class="text-gray-600 ml-2 font-normal italic lowercase">(since ${data.uptime})</span>`;
  390. text.classList.replace('text-gray-500', 'text-green-400');
  391. text.classList.replace('text-red-500', 'text-green-400'); // Ασφαλής αλλαγή
  392. } else {
  393. dot.className = "w-2.5 h-2.5 rounded-full bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.8)]";
  394. text.innerText = "Service: Offline";
  395. text.classList.replace('text-green-400', 'text-red-500');
  396. text.classList.replace('text-gray-500', 'text-red-500');
  397. }
  398. }
  399. }).catch(e => console.log('Status check error:', e)); // Προστέθηκε catch
  400. }
  401. function restoreDashboardState() { switchTab(activeTab); updateLiveStatus(); }
  402. document.addEventListener("DOMContentLoaded", () => {
  403. // Μόνο οι Admin βλέπουν τα charts
  404. if(document.getElementById('rpsChart')) {
  405. renderLiveCharts();
  406. setInterval(renderLiveCharts, 10000);
  407. }
  408. // Ο έλεγχος του service τρέχει μόνο αν υπάρχει το dot
  409. if(document.getElementById('status-dot')) {
  410. setInterval(updateLiveStatus, 5000);
  411. }
  412. restoreDashboardState();
  413. });
  414. </script>
  415. {% endblock %}