index.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Visa Plugin Manager</title>
  7. <!-- 引入 Tailwind CSS -->
  8. <script src="https://cdn.tailwindcss.com"></script>
  9. <!-- 引入 Vue 3 -->
  10. <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  11. <!-- 引入 Axios -->
  12. <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  13. <style>
  14. [v-cloak] { display: none; }
  15. </style>
  16. </head>
  17. <body class="bg-gray-100 min-h-screen font-sans">
  18. <div id="app" v-cloak class="container mx-auto px-4 py-8">
  19. <!-- 头部 -->
  20. <header class="flex justify-between items-center mb-8 bg-white p-6 rounded-lg shadow-md">
  21. <div>
  22. <h1 class="text-2xl font-bold text-gray-800">Visa Plugin Manager</h1>
  23. <p class="text-gray-500 text-sm mt-1">Status Monitor & Control Panel</p>
  24. </div>
  25. <div class="flex gap-3">
  26. <button @click="reloadConfig" :disabled="loading" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded shadow transition flex items-center">
  27. <span v-if="loading">...</span>
  28. <span v-else>Reload Config</span>
  29. </button>
  30. <button @click="fetchStatus" :disabled="loading" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded shadow transition">
  31. Refresh Status
  32. </button>
  33. </div>
  34. </header>
  35. <!-- OTA 面板 -->
  36. <div class="mb-8 bg-white p-6 rounded-lg shadow-md">
  37. <h2 class="text-lg font-semibold mb-4 text-gray-700 border-b pb-2">OTA Plugin Update</h2>
  38. <div class="flex gap-4 items-end">
  39. <div class="flex-1">
  40. <label class="block text-sm font-medium text-gray-700 mb-1">Plugin Name</label>
  41. <input v-model="otaPluginName" type="text" placeholder="e.g. bls_plugin" class="w-full border-gray-300 border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
  42. <p class="text-xs text-gray-500 mt-1">Make sure you have replaced the .py file in plugins/ directory first.</p>
  43. </div>
  44. <button @click="triggerOTA" :disabled="!otaPluginName || loading" class="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded shadow transition mb-[2px]">
  45. Hot Reload Plugin
  46. </button>
  47. </div>
  48. </div>
  49. <!-- 任务组列表 -->
  50. <div class="bg-white rounded-lg shadow-md overflow-hidden">
  51. <div class="p-6 border-b border-gray-200 flex justify-between items-center">
  52. <h2 class="text-lg font-semibold text-gray-700">Task Groups</h2>
  53. <span class="text-sm bg-gray-100 text-gray-600 px-3 py-1 rounded-full">{{ groups.length }} Groups</span>
  54. </div>
  55. <div class="overflow-x-auto">
  56. <table class="w-full text-left border-collapse">
  57. <thead>
  58. <tr class="bg-gray-50 text-gray-600 text-sm uppercase tracking-wider">
  59. <th class="px-6 py-4 font-medium">Group ID</th>
  60. <th class="px-6 py-4 font-medium">Plugin</th>
  61. <th class="px-6 py-4 font-medium">Pool</th>
  62. <th class="px-6 py-4 font-medium text-center">Instances</th>
  63. <th class="px-6 py-4 font-medium text-center">Status</th>
  64. <th class="px-6 py-4 font-medium text-right">Actions</th>
  65. </tr>
  66. </thead>
  67. <tbody class="divide-y divide-gray-200">
  68. <tr v-for="g in groups" :key="g.id" class="hover:bg-gray-50 transition">
  69. <td class="px-6 py-4 font-medium text-gray-900">{{ g.id }}</td>
  70. <td class="px-6 py-4 text-gray-600 font-mono text-sm">{{ g.plugin }}</td>
  71. <td class="px-6 py-4 text-gray-600">{{ g.account_pool }}</td>
  72. <td class="px-6 py-4 text-center">
  73. <span class="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-bold">
  74. {{ g.instances }}
  75. </span>
  76. </td>
  77. <td class="px-6 py-4 text-center">
  78. <span v-if="g.running" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
  79. <span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
  80. Running
  81. </span>
  82. <span v-else class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
  83. <span class="w-2 h-2 mr-1 bg-red-500 rounded-full"></span>
  84. Stopped
  85. </span>
  86. </td>
  87. <td class="px-6 py-4 text-right space-x-2">
  88. <button v-if="!g.running" @click="controlGroup(g.id, 'start')" class="text-green-600 hover:text-green-900 font-medium text-sm hover:underline">Start</button>
  89. <button v-if="g.running" @click="controlGroup(g.id, 'restart')" class="text-orange-600 hover:text-orange-900 font-medium text-sm hover:underline">Restart</button>
  90. <button v-if="g.running" @click="controlGroup(g.id, 'stop')" class="text-red-600 hover:text-red-900 font-medium text-sm hover:underline">Stop</button>
  91. </td>
  92. </tr>
  93. <tr v-if="groups.length === 0">
  94. <td colspan="6" class="px-6 py-8 text-center text-gray-500">No groups loaded. Check config/groups.json</td>
  95. </tr>
  96. </tbody>
  97. </table>
  98. </div>
  99. </div>
  100. </div>
  101. <script>
  102. const { createApp, ref, onMounted } = Vue;
  103. createApp({
  104. setup() {
  105. const groups = ref([]);
  106. const loading = ref(false);
  107. const otaPluginName = ref("");
  108. // 获取状态
  109. const fetchStatus = async () => {
  110. try {
  111. loading.value = true;
  112. const res = await axios.get('/status');
  113. groups.value = res.data.data;
  114. } catch (err) {
  115. alert("Failed to fetch status: " + err.message);
  116. } finally {
  117. loading.value = false;
  118. }
  119. };
  120. // 统一的控制函数
  121. const controlGroup = async (groupId, action) => {
  122. try {
  123. loading.value = true;
  124. // action: 'start', 'stop', 'restart'
  125. const res = await axios.post(`/${action}`, { group_id: groupId });
  126. // 操作后稍微等待一下再刷新,让后台状态变化
  127. setTimeout(() => {
  128. fetchStatus();
  129. alert(`${action.toUpperCase()} command sent.`);
  130. }, 500);
  131. } catch (err) {
  132. const msg = err.response?.data?.detail || err.message;
  133. alert(`Failed to ${action}: ${msg}`);
  134. loading.value = false;
  135. }
  136. };
  137. // 重载配置
  138. const reloadConfig = async () => {
  139. if (!confirm("Are you sure to reload config? This won't stop running groups.")) return;
  140. try {
  141. loading.value = true;
  142. await axios.post('/reload_config');
  143. await fetchStatus();
  144. alert("Configuration reloaded.");
  145. } catch (err) {
  146. alert("Error: " + err.message);
  147. } finally {
  148. loading.value = false;
  149. }
  150. };
  151. // OTA 更新
  152. const triggerOTA = async () => {
  153. if (!otaPluginName.value) return;
  154. if (!confirm(`Confirm hot reload for plugin '${otaPluginName.value}'? This will restart related groups.`)) return;
  155. try {
  156. loading.value = true;
  157. const res = await axios.post('/ota', { plugin_name: otaPluginName.value });
  158. let msg = res.data.message;
  159. if (res.data.restarted_groups.length > 0) {
  160. msg += `\nRestarted groups: ${res.data.restarted_groups.join(', ')}`;
  161. } else {
  162. msg += "\nNo active groups were using this plugin.";
  163. }
  164. await fetchStatus();
  165. alert(msg);
  166. } catch (err) {
  167. alert("OTA Error: " + err.message);
  168. } finally {
  169. loading.value = false;
  170. }
  171. };
  172. onMounted(() => {
  173. fetchStatus();
  174. // 自动刷新 (可选,每5秒)
  175. setInterval(fetchStatus, 5000);
  176. });
  177. return {
  178. groups,
  179. loading,
  180. otaPluginName,
  181. fetchStatus,
  182. controlGroup,
  183. reloadConfig,
  184. triggerOTA
  185. };
  186. }
  187. }).mount('#app');
  188. </script>
  189. </body>
  190. </html>