| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Visa Plugin Manager</title>
- <!-- 引入 Tailwind CSS -->
- <script src="https://cdn.tailwindcss.com"></script>
- <!-- 引入 Vue 3 -->
- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
- <!-- 引入 Axios -->
- <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
- <style>
- [v-cloak] { display: none; }
- </style>
- </head>
- <body class="bg-gray-100 min-h-screen font-sans">
- <div id="app" v-cloak class="container mx-auto px-4 py-8">
-
- <!-- 头部 -->
- <header class="flex justify-between items-center mb-8 bg-white p-6 rounded-lg shadow-md">
- <div>
- <h1 class="text-2xl font-bold text-gray-800">Visa Plugin Manager</h1>
- <p class="text-gray-500 text-sm mt-1">Status Monitor & Control Panel</p>
- </div>
- <div class="flex gap-3">
- <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">
- <span v-if="loading">...</span>
- <span v-else>Reload Config</span>
- </button>
- <button @click="fetchStatus" :disabled="loading" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded shadow transition">
- Refresh Status
- </button>
- </div>
- </header>
- <!-- OTA 面板 -->
- <div class="mb-8 bg-white p-6 rounded-lg shadow-md">
- <h2 class="text-lg font-semibold mb-4 text-gray-700 border-b pb-2">OTA Plugin Update</h2>
- <div class="flex gap-4 items-end">
- <div class="flex-1">
- <label class="block text-sm font-medium text-gray-700 mb-1">Plugin Name</label>
- <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">
- <p class="text-xs text-gray-500 mt-1">Make sure you have replaced the .py file in plugins/ directory first.</p>
- </div>
- <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]">
- Hot Reload Plugin
- </button>
- </div>
- </div>
- <!-- 任务组列表 -->
- <div class="bg-white rounded-lg shadow-md overflow-hidden">
- <div class="p-6 border-b border-gray-200 flex justify-between items-center">
- <h2 class="text-lg font-semibold text-gray-700">Task Groups</h2>
- <span class="text-sm bg-gray-100 text-gray-600 px-3 py-1 rounded-full">{{ groups.length }} Groups</span>
- </div>
-
- <div class="overflow-x-auto">
- <table class="w-full text-left border-collapse">
- <thead>
- <tr class="bg-gray-50 text-gray-600 text-sm uppercase tracking-wider">
- <th class="px-6 py-4 font-medium">Group ID</th>
- <th class="px-6 py-4 font-medium">Plugin</th>
- <th class="px-6 py-4 font-medium">Pool</th>
- <th class="px-6 py-4 font-medium text-center">Instances</th>
- <th class="px-6 py-4 font-medium text-center">Status</th>
- <th class="px-6 py-4 font-medium text-right">Actions</th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200">
- <tr v-for="g in groups" :key="g.id" class="hover:bg-gray-50 transition">
- <td class="px-6 py-4 font-medium text-gray-900">{{ g.id }}</td>
- <td class="px-6 py-4 text-gray-600 font-mono text-sm">{{ g.plugin }}</td>
- <td class="px-6 py-4 text-gray-600">{{ g.account_pool }}</td>
- <td class="px-6 py-4 text-center">
- <span class="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-bold">
- {{ g.instances }}
- </span>
- </td>
- <td class="px-6 py-4 text-center">
- <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">
- <span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
- Running
- </span>
- <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">
- <span class="w-2 h-2 mr-1 bg-red-500 rounded-full"></span>
- Stopped
- </span>
- </td>
- <td class="px-6 py-4 text-right space-x-2">
- <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>
- <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>
- <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>
- </td>
- </tr>
- <tr v-if="groups.length === 0">
- <td colspan="6" class="px-6 py-8 text-center text-gray-500">No groups loaded. Check config/groups.json</td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- <script>
- const { createApp, ref, onMounted } = Vue;
- createApp({
- setup() {
- const groups = ref([]);
- const loading = ref(false);
- const otaPluginName = ref("");
- // 获取状态
- const fetchStatus = async () => {
- try {
- loading.value = true;
- const res = await axios.get('/status');
- groups.value = res.data.data;
- } catch (err) {
- alert("Failed to fetch status: " + err.message);
- } finally {
- loading.value = false;
- }
- };
- // 统一的控制函数
- const controlGroup = async (groupId, action) => {
- try {
- loading.value = true;
- // action: 'start', 'stop', 'restart'
- const res = await axios.post(`/${action}`, { group_id: groupId });
- // 操作后稍微等待一下再刷新,让后台状态变化
- setTimeout(() => {
- fetchStatus();
- alert(`${action.toUpperCase()} command sent.`);
- }, 500);
- } catch (err) {
- const msg = err.response?.data?.detail || err.message;
- alert(`Failed to ${action}: ${msg}`);
- loading.value = false;
- }
- };
- // 重载配置
- const reloadConfig = async () => {
- if (!confirm("Are you sure to reload config? This won't stop running groups.")) return;
- try {
- loading.value = true;
- await axios.post('/reload_config');
- await fetchStatus();
- alert("Configuration reloaded.");
- } catch (err) {
- alert("Error: " + err.message);
- } finally {
- loading.value = false;
- }
- };
- // OTA 更新
- const triggerOTA = async () => {
- if (!otaPluginName.value) return;
- if (!confirm(`Confirm hot reload for plugin '${otaPluginName.value}'? This will restart related groups.`)) return;
-
- try {
- loading.value = true;
- const res = await axios.post('/ota', { plugin_name: otaPluginName.value });
-
- let msg = res.data.message;
- if (res.data.restarted_groups.length > 0) {
- msg += `\nRestarted groups: ${res.data.restarted_groups.join(', ')}`;
- } else {
- msg += "\nNo active groups were using this plugin.";
- }
-
- await fetchStatus();
- alert(msg);
- } catch (err) {
- alert("OTA Error: " + err.message);
- } finally {
- loading.value = false;
- }
- };
- onMounted(() => {
- fetchStatus();
- // 自动刷新 (可选,每5秒)
- setInterval(fetchStatus, 5000);
- });
- return {
- groups,
- loading,
- otaPluginName,
- fetchStatus,
- controlGroup,
- reloadConfig,
- triggerOTA
- };
- }
- }).mount('#app');
- </script>
- </body>
- </html>
|