Web-UI: Schedules support
This commit is contained in:
@@ -90,19 +90,37 @@ async def add_cron_task(
|
|||||||
@router.get("/tasks")
|
@router.get("/tasks")
|
||||||
async def get_all_tasks():
|
async def get_all_tasks():
|
||||||
jobs = []
|
jobs = []
|
||||||
|
|
||||||
for job in scheduler.get_jobs():
|
for job in scheduler.get_jobs():
|
||||||
|
# Разбираем строку типа "Group: bedroom | {'state': True}"
|
||||||
|
# Вытаскиваем цель (bedroom) и состояние (True/False)
|
||||||
|
name_parts = job.name.split("|")
|
||||||
|
target = name_parts[0].replace("Group:", "").replace("Device:", "").strip()
|
||||||
|
|
||||||
|
# Пытаемся понять, ВКЛ или ВЫКЛ задача
|
||||||
|
is_on = "True" in job.name
|
||||||
|
|
||||||
jobs.append(
|
jobs.append(
|
||||||
{
|
{
|
||||||
"id": job.id,
|
"id": job.id,
|
||||||
"name": job.name,
|
"target_id": target,
|
||||||
|
"state": is_on,
|
||||||
|
# Достаем время из триггера APScheduler
|
||||||
"next_run": (
|
"next_run": (
|
||||||
job.next_run_time.isoformat() if job.next_run_time else None
|
job.next_run_time.isoformat() if job.next_run_time else None
|
||||||
),
|
),
|
||||||
"params": str(job.args[1]) if len(job.args) > 1 else None,
|
# Вытаскиваем час и минуту прямо из настроек триггера для красоты
|
||||||
|
"hour": (
|
||||||
|
str(job.trigger.fields[5]).zfill(2)
|
||||||
|
if hasattr(job.trigger, "fields")
|
||||||
|
else "??"
|
||||||
|
),
|
||||||
|
"minute": (
|
||||||
|
str(job.trigger.fields[6]).zfill(2)
|
||||||
|
if hasattr(job.trigger, "fields")
|
||||||
|
else "??"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"tasks": jobs}
|
return {"tasks": jobs}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,10 +58,68 @@
|
|||||||
|
|
||||||
<nav class="flex glass p-1.5 rounded-2xl">
|
<nav class="flex glass p-1.5 rounded-2xl">
|
||||||
<button @click="tab = 'control'" :class="tab === 'control' ? 'active-tab' : 'text-slate-400'" class="px-8 py-2.5 rounded-xl font-bold transition-all">ПУЛЬТ</button>
|
<button @click="tab = 'control'" :class="tab === 'control' ? 'active-tab' : 'text-slate-400'" class="px-8 py-2.5 rounded-xl font-bold transition-all">ПУЛЬТ</button>
|
||||||
|
<button @click="tab = 'schedules'" :class="tab === 'schedules' ? 'active-tab' : 'text-slate-400'" class="px-8 py-2.5 rounded-xl font-bold transition-all">ГРАФИК</button>
|
||||||
<button @click="tab = 'admin'" :class="tab === 'admin' ? 'active-tab' : 'text-slate-400'" class="px-8 py-2.5 rounded-xl font-bold transition-all">АДМИНКА</button>
|
<button @click="tab = 'admin'" :class="tab === 'admin' ? 'active-tab' : 'text-slate-400'" class="px-8 py-2.5 rounded-xl font-bold transition-all">АДМИНКА</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div v-if="tab === 'schedules'" class="space-y-10">
|
||||||
|
<div class="glass p-8 rounded-[2.5rem]">
|
||||||
|
<h2 class="text-2xl font-black mb-6 uppercase italic">Новая задача</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<select v-model="newTask.target_id" class="bg-slate-900 border border-slate-700 p-4 rounded-xl outline-none focus:border-orange-500">
|
||||||
|
<option value="" disabled>Выберите группу...</option>
|
||||||
|
<option v-for="(g, id) in groups" :value="id">{{ g.name }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 bg-slate-900 border border-slate-700 p-2 rounded-xl">
|
||||||
|
<select v-model="taskHour" class="bg-transparent outline-none flex-1 text-center font-bold">
|
||||||
|
<option v-for="h in 24" :value="(h-1).toString().padStart(2, '0')">{{ (h-1).toString().padStart(2, '0') }}</option>
|
||||||
|
</select>
|
||||||
|
<span class="font-bold">:</span>
|
||||||
|
<select v-model="taskMin" class="bg-transparent outline-none flex-1 text-center font-bold">
|
||||||
|
<option v-for="m in 60" :value="(m-1).toString().padStart(2, '0')">{{ (m-1).toString().padStart(2, '0') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select v-model="newTask.state" class="bg-slate-900 border border-slate-700 p-4 rounded-xl outline-none">
|
||||||
|
<option :value="true">ВКЛЮЧИТЬ</option>
|
||||||
|
<option :value="false">ВЫКЛЮЧИТЬ</option>
|
||||||
|
</select>
|
||||||
|
<button @click="addSchedule" class="bg-orange-600 hover:bg-orange-500 rounded-xl font-black transition-all">ДОБАВИТЬ</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass p-8 rounded-[2.5rem]">
|
||||||
|
<h2 class="text-2xl font-black mb-6 uppercase italic">Активные задачи</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div v-for="task in tasks" :key="task.id" class="bg-slate-900/50 border border-slate-800 p-6 rounded-2xl flex justify-between items-center group">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3 mb-1">
|
||||||
|
<span class="text-orange-500 text-xl font-black tracking-tighter">{{ task.hour }}:{{ task.minute }}</span>
|
||||||
|
<span class="text-[10px] bg-slate-800 px-2 py-0.5 rounded text-slate-400 uppercase font-bold">
|
||||||
|
{{ groups[task.target_id]?.name || task.target_id }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span :class="task.state ? 'text-green-400' : 'text-red-400'" class="text-xs font-bold uppercase">
|
||||||
|
{{ task.state ? 'ВКЛЮЧИТЬ' : 'ВЫКЛЮЧИТЬ' }}
|
||||||
|
</span>
|
||||||
|
<span class="text-[10px] text-slate-600">—</span>
|
||||||
|
<span class="text-[10px] text-slate-500 uppercase tracking-widest">
|
||||||
|
{{ task.next_run ? 'След. запуск: ' + new Date(task.next_run).toLocaleDateString() : 'Разовая задача' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="deleteTask(task.id)" class="text-slate-600 hover:text-red-500 p-2 transition-colors">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path 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" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="tasks.length === 0" class="text-center py-10 text-slate-500 italic">Задач пока нет</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="tab === 'control'" class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div v-if="tab === 'control'" class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<div v-if="Object.keys(groups).length === 0" class="col-span-full text-center py-20 glass rounded-3xl opacity-50">
|
<div v-if="Object.keys(groups).length === 0" class="col-span-full text-center py-20 glass rounded-3xl opacity-50">
|
||||||
<p class="text-xl italic text-slate-400">Создайте группу в админке, чтобы управлять лампами</p>
|
<p class="text-xl italic text-slate-400">Создайте группу в админке, чтобы управлять лампами</p>
|
||||||
@@ -159,6 +217,10 @@
|
|||||||
sliders: {},
|
sliders: {},
|
||||||
newGroup: { id: '', name: '', macs: [] },
|
newGroup: { id: '', name: '', macs: [] },
|
||||||
isLoadingStatus: false,
|
isLoadingStatus: false,
|
||||||
|
taskHour: '12', // Начальное значение часа
|
||||||
|
taskMin: '00', // Начальное значение минут
|
||||||
|
newTask: { target_id: '', time: '', state: true },
|
||||||
|
tasks: [],
|
||||||
allScenes: [
|
allScenes: [
|
||||||
"ocean", "romance", "party", "fireplace", "cozy", "forest",
|
"ocean", "romance", "party", "fireplace", "cozy", "forest",
|
||||||
"pastel_colors", "wake_up", "bedtime", "warm_white", "daylight",
|
"pastel_colors", "wake_up", "bedtime", "warm_white", "daylight",
|
||||||
@@ -219,6 +281,7 @@
|
|||||||
await this.syncGroupStatuses();
|
await this.syncGroupStatuses();
|
||||||
}
|
}
|
||||||
if (dData) this.devices = Object.values(dData);
|
if (dData) this.devices = Object.values(dData);
|
||||||
|
this.fetchTasks();
|
||||||
},
|
},
|
||||||
async control(id, params) {
|
async control(id, params) {
|
||||||
await this.request(`/control/group/${id}`, 'POST', params);
|
await this.request(`/control/group/${id}`, 'POST', params);
|
||||||
@@ -280,6 +343,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.isLoadingStatus = false;
|
this.isLoadingStatus = false;
|
||||||
|
},
|
||||||
|
async fetchTasks() {
|
||||||
|
const data = await this.request('/schedules/tasks');
|
||||||
|
if (data) this.tasks = data.tasks;
|
||||||
|
},
|
||||||
|
async addSchedule() {
|
||||||
|
// Проверяем, выбрана ли группа
|
||||||
|
if (!this.newTask.target_id) {
|
||||||
|
alert("Выбери группу!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем данные. Теперь берем их прямо из taskHour и taskMin
|
||||||
|
await this.request('/schedules/cron', 'POST', {
|
||||||
|
target_id: this.newTask.target_id,
|
||||||
|
hour: this.taskHour,
|
||||||
|
minute: this.taskMin,
|
||||||
|
is_group: true,
|
||||||
|
state: this.newTask.state
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchTasks(); // Обновляем список
|
||||||
|
},
|
||||||
|
async deleteTask(id) {
|
||||||
|
await this.request(`/schedules/${id}`, 'DELETE');
|
||||||
|
this.fetchTasks();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
Reference in New Issue
Block a user