DELETE groups

This commit is contained in:
Артём Кокос
2026-02-12 23:37:15 +07:00
parent 87e03fdb26
commit 298dcbc277
2 changed files with 185 additions and 84 deletions

53
main.py
View File

@@ -174,9 +174,60 @@ async def control_group(
return {"status": "ok", "applied": params, "sent_to": ips} return {"status": "ok", "applied": params, "sent_to": ips}
# Монтируем папку static для фронтенда @app.delete("/groups/{group_id}")
async def delete_group(group_id: str):
async with async_session() as session:
# Ищем в базе
result = await session.execute(
select(GroupModel).where(GroupModel.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
raise HTTPException(status_code=404, detail="Группа не найдена")
await session.delete(group)
await session.commit()
# Удаляем из оперативной памяти
if group_id in state_manager.groups:
del state_manager.groups[group_id]
return {"status": "deleted", "id": group_id}
@app.post("/discovery/rescan")
async def rescan_network():
logger.info("🔄 Ручной перезапуск сканирования сети...")
found_devices = await discovery.scan_network()
for dev_data in found_devices:
state_manager.update_device(dev_data)
return {"status": "ok", "found": len(state_manager.devices)}
@app.post("/control/device/{device_id}/blink")
async def blink_device(device_id: str):
device = state_manager.devices.get(device_id)
if not device:
raise HTTPException(status_code=404, detail="Лампа оффлайн")
# Сцена 34 в WiZ — это пульсация/мигание
await wiz.set_pilot(device.ip, {"sceneId": 34, "speed": 100})
# Через 3 секунды выключаем, чтобы не мигала вечно
await asyncio.sleep(3)
await wiz.set_pilot(device.ip, {"state": False})
return {"status": "blink_sent"}
# --- МОНТИРОВАНИЕ СТАТИКИ (ДОЛЖНО БЫТЬ ПОСЛЕ ВСЕХ API МАРШРУТОВ) ---
app.mount("/", StaticFiles(directory="static", html=True), name="static") app.mount("/", StaticFiles(directory="static", html=True), name="static")
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn

View File

@@ -8,87 +8,128 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<style> <style>
body { background: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; } body { background: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
.card { background: #1e293b; border: 1px solid #334155; transition: transform 0.2s; } .card { background: #1e293b; border: 1px solid #334155; }
.card:hover { transform: translateY(-2px); } .btn-primary { @apply bg-orange-600 hover:bg-orange-500 text-white px-4 py-2 rounded-xl font-bold transition-all disabled:opacity-50; }
input[type="range"] { -webkit-appearance: none; height: 8px; border-radius: 4px; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: white; border-radius: 50%; cursor: pointer; border: 2px solid #f97316; } /* ЖЕСТКО ЗАДАННЫЕ СТИЛИ ДЛЯ ИНПУТОВ */
.input-dark {
background-color: #0f172a !important;
color: #ffffff !important;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
outline: none;
width: 100%;
}
.input-dark:focus { border-color: #f97316; }
.input-dark::placeholder { color: #64748b; }
.temp-gradient { background: linear-gradient(to right, #ffb366, #ffffff, #99ccff); } .temp-gradient { background: linear-gradient(to right, #ffb366, #ffffff, #99ccff); }
input[type="range"] { -webkit-appearance: none; height: 8px; border-radius: 4px; background: #334155; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: white; border-radius: 50%; border: 2px solid #f97316; cursor: pointer; }
</style> </style>
</head> </head>
<body> <body>
<div id="app" class="p-4 md:p-8 max-w-5xl mx-auto"> <div id="app" class="p-4 md:p-8 max-w-5xl mx-auto">
<header class="mb-10 flex flex-col md:flex-row justify-between items-center gap-4"> <header class="mb-10 flex justify-between items-center">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="text-4xl">🔥</span> <span class="text-4xl">🔥</span>
<h1 class="text-3xl font-black text-orange-500 tracking-tighter uppercase">Ignis Core</h1> <h1 class="text-3xl font-black text-orange-500 tracking-tighter uppercase">Ignis</h1>
</div> </div>
<div class="flex items-center gap-6 bg-slate-800/50 px-6 py-3 rounded-2xl border border-slate-700"> <div class="flex gap-2 bg-slate-800/50 p-1 rounded-2xl border border-slate-700">
<div class="text-center"> <button @click="tab = 'control'" :class="tab === 'control' ? 'bg-slate-700 text-white shadow-lg' : 'text-slate-400'" class="px-6 py-2 rounded-xl font-bold transition-all">Пульт</button>
<div class="text-xs text-slate-400 uppercase font-bold">Устройств</div> <button @click="tab = 'admin'" :class="tab === 'admin' ? 'bg-slate-700 text-white shadow-lg' : 'text-slate-400'" class="px-6 py-2 rounded-xl font-bold transition-all">Настройки</button>
<div class="text-xl font-mono text-orange-400">{{ devicesCount }}</div>
</div>
<div class="h-8 w-[1px] bg-slate-700"></div>
<button @click="allOff" class="text-xs bg-red-500/10 hover:bg-red-500/20 text-red-400 px-4 py-2 rounded-lg border border-red-500/20 transition-all">
ВЫКЛЮЧИТЬ ВСЁ
</button>
</div> </div>
</header> </header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <main v-if="tab === 'control'" class="space-y-6">
<div v-for="(group, id) in groups" :key="id" class="card p-6 rounded-3xl shadow-2xl relative overflow-hidden"> <div v-if="Object.keys(groups).length === 0" class="text-center py-20 opacity-50 uppercase tracking-widest">Групп пока нет. Зайди в настройки.</div>
<div class="flex justify-between items-center mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div> <div v-for="(group, id) in groups" :key="id" class="card p-6 rounded-3xl shadow-2xl">
<h2 class="text-2xl font-bold tracking-tight">{{ group.name }}</h2> <div class="flex justify-between items-center mb-6">
<span class="text-[10px] text-slate-500 font-mono">{{ id }}</span> <h2 class="text-2xl font-bold text-slate-100">{{ group.name }}</h2>
<div class="flex gap-2">
<button @click="toggleGroup(id, true)" class="bg-orange-600 hover:bg-orange-500 px-4 py-1 rounded-lg text-sm font-bold transition-all">ВКЛ</button>
<button @click="toggleGroup(id, false)" class="bg-slate-700 hover:bg-slate-600 px-4 py-1 rounded-lg text-sm font-bold transition-all">ВЫКЛ</button>
</div>
</div> </div>
<div class="flex gap-2">
<button @click="toggleGroup(id, true)" class="bg-orange-600 hover:bg-orange-500 text-white px-5 py-2 rounded-xl text-sm font-bold transition-colors shadow-lg shadow-orange-900/20">ВКЛ</button> <div class="space-y-6">
<button @click="toggleGroup(id, false)" class="bg-slate-700 hover:bg-slate-600 text-white px-5 py-2 rounded-xl text-sm font-bold transition-colors">ВЫКЛ</button> <div>
<div class="flex justify-between text-[10px] font-black uppercase text-slate-500 mb-2"><span>Яркость</span><span class="text-orange-400">{{ group.brightness || 100 }}%</span></div>
<input type="range" min="10" max="100" class="w-full" v-model="group.brightness" @change="setBrightness(id, $event.target.value)">
</div>
<div>
<div class="flex justify-between text-[10px] font-black uppercase text-slate-500 mb-2"><span>Температура</span><span class="text-blue-400">{{ group.temp || 3000 }}K</span></div>
<input type="range" min="2700" max="6500" step="100" class="w-full temp-gradient" v-model="group.temp" @change="setTemp(id, $event.target.value)">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-[10px] font-black uppercase text-slate-500 mb-2 block">Цвет</label>
<input type="color" class="w-full h-10 bg-transparent border border-slate-700 rounded-lg cursor-pointer" @input="setColor(id, $event.target.value)">
</div>
<div>
<label class="text-[10px] font-black uppercase text-slate-500 mb-2 block">Сцены</label>
<div class="flex flex-wrap gap-1">
<button v-for="s in ['ocean', 'party', 'steampunk', 'fireplace']" @click="setScene(id, s)" class="text-[8px] font-bold border border-slate-700 px-2 py-1 rounded hover:bg-slate-800 uppercase transition-all">{{s}}</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div>
</main>
<div class="space-y-8"> <main v-if="tab === 'admin'" class="space-y-10">
<div> <section class="card p-6 rounded-3xl">
<div class="flex justify-between mb-2"> <div class="flex justify-between items-center mb-6">
<label class="text-xs font-black text-slate-400 uppercase tracking-widest">Яркость</label> <h2 class="text-xl font-bold italic text-orange-400">Новая группа</h2>
<span class="text-xs font-mono text-orange-500">{{ group.brightness || 100 }}%</span> <button @click="rescan" class="text-[10px] font-black bg-blue-600/10 text-blue-400 border border-blue-500/20 px-4 py-2 rounded-xl hover:bg-blue-600/20 transition-all">
</div> 🔄 ПЕРЕСКАН СЕТИ
<input type="range" min="10" max="100" class="w-full bg-slate-800 accent-orange-500" </button>
v-model="group.brightness" </div>
@change="setBrightness(id, $event.target.value)">
</div> <div class="flex flex-col md:flex-row gap-4 mb-8">
<input v-model="newGroup.id" placeholder="ID (например: kitchen)" class="input-dark">
<input v-model="newGroup.name" placeholder="Имя (например: Кухня)" class="input-dark">
<button @click="createGroup" :disabled="!newGroup.id || !newGroup.macs.length" class="btn-primary min-w-[140px]">СОЗДАТЬ</button>
</div>
<div> <div>
<div class="flex justify-between mb-2"> <p class="text-xs font-black text-slate-500 mb-4 uppercase tracking-widest">Доступные лампы в сети:</p>
<label class="text-xs font-black text-slate-400 uppercase tracking-widest">Температура</label> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<span class="text-xs font-mono text-blue-400">{{ group.temp || 3000 }}K</span> <div v-for="dev in devices" :key="dev.id"
</div> class="flex items-center justify-between p-4 bg-slate-900/50 rounded-2xl border border-slate-700 hover:border-slate-500 transition-all">
<input type="range" min="2700" max="6500" step="100" class="w-full temp-gradient accent-white" <label class="flex items-center gap-3 cursor-pointer flex-1">
v-model="group.temp" <input type="checkbox" :value="dev.id" v-model="newGroup.macs" class="w-5 h-5 accent-orange-500">
@change="setTemp(id, $event.target.value)"> <div class="text-xs">
</div> <div class="font-bold text-slate-200">{{ dev.id }}</div>
<div class="text-slate-500 font-mono text-[10px]">{{ dev.ip }}</div>
<div class="grid grid-cols-2 gap-4 items-end"> </div>
<div class="w-full"> </label>
<label class="text-xs font-black text-slate-400 uppercase tracking-widest mb-2 block">Цвет RGB</label> <button @click="blink(dev.id)" class="p-2 hover:bg-orange-500/20 rounded-xl text-orange-400 transition-all">
<input type="color" class="w-full h-12 bg-transparent border-2 border-slate-700 rounded-xl cursor-pointer" 👁️
@input="setColor(id, $event.target.value)">
</div>
<div class="flex flex-wrap gap-2">
<button v-for="s in ['ocean', 'fireplace', 'party', 'steampunk']"
@click="setScene(id, s)"
class="flex-1 text-[9px] font-bold uppercase py-2 px-1 rounded-lg border border-slate-700 hover:bg-slate-800 transition-all">
{{ s }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</div>
<div v-if="Object.keys(groups).length === 0" class="text-center py-20 bg-slate-800/20 rounded-3xl border-2 border-dashed border-slate-700"> <section class="card p-6 rounded-3xl">
<p class="text-slate-500 uppercase tracking-widest font-bold">Группы не найдены в базе</p> <h2 class="text-xl font-bold mb-6 text-slate-400 uppercase tracking-tighter">Управление группами в базе</h2>
</div> <div class="grid grid-cols-1 gap-3">
<div v-for="(group, id) in groups" :key="id" class="flex justify-between items-center p-5 bg-slate-900/30 rounded-2xl border border-slate-800">
<div>
<div class="font-bold text-slate-100 text-lg">{{ group.name }}</div>
<div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">{{ id }} • {{ group.device_ids.length }} ламп</div>
</div>
<button @click="deleteGroup(id)" class="text-red-500 text-[10px] font-black border border-red-500/20 px-4 py-2 rounded-xl hover:bg-red-500 hover:text-white transition-all">
УДАЛИТЬ
</button>
</div>
</div>
</section>
</main>
</div> </div>
<script> <script>
@@ -96,52 +137,61 @@
createApp({ createApp({
data() { data() {
return { return {
tab: 'control',
groups: {}, groups: {},
devicesCount: 0 devices: {},
newGroup: { id: '', name: '', macs: [] }
} }
}, },
methods: { methods: {
async fetchData() { async fetchData() {
try { try {
const gResp = await fetch('/groups'); const gResp = await fetch('/groups');
const gData = await gResp.json(); this.groups = await gResp.json();
// Сохраняем значения ползунков, чтобы они не прыгали при обновлении
for (let id in gData) {
if (this.groups[id]) {
gData[id].brightness = this.groups[id].brightness;
gData[id].temp = this.groups[id].temp;
}
}
this.groups = gData;
const dResp = await fetch('/devices'); const dResp = await fetch('/devices');
const dData = await dResp.json(); this.devices = await dResp.json();
this.devicesCount = Object.keys(dData).length; } catch (e) { console.error("Ошибка сети", e); }
} catch (e) { console.error("Ошибка обновления данных", e); }
}, },
async control(id, params) { async control(id, params) {
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
await fetch(`/control/group/${id}?${query}`, { method: 'POST' }); await fetch(`/control/group/${id}?${query}`, { method: 'POST' });
}, },
async createGroup() {
const resp = await fetch('/groups', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id: this.newGroup.id, name: this.newGroup.name, macs: this.newGroup.macs })
});
if(resp.ok) {
this.newGroup = { id: '', name: '', macs: [] };
await this.fetchData();
this.tab = 'control';
}
},
async deleteGroup(id) {
if(!confirm(`Удалить группу "${id}" из базы навсегда?`)) return;
const resp = await fetch(`/groups/${id}`, { method: 'DELETE' });
if(resp.ok) await this.fetchData();
},
async rescan() {
await fetch('/discovery/rescan', { method: 'POST' });
await this.fetchData();
},
async blink(deviceId) {
await fetch(`/control/device/${deviceId}/blink`, { method: 'POST' });
},
toggleGroup(id, state) { this.control(id, { state }); }, toggleGroup(id, state) { this.control(id, { state }); },
setBrightness(id, val) { this.control(id, { brightness: val }); }, setBrightness(id, val) { this.control(id, { brightness: val }); },
setTemp(id, val) { this.control(id, { temp: val }); }, setTemp(id, val) { this.control(id, { temp: val }); },
setScene(id, scene) { this.control(id, { scene }); }, setScene(id, scene) { this.control(id, { scene }); },
setColor(id, hex) { setColor(id, hex) {
const r = parseInt(hex.slice(1, 3), 16); const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
this.control(id, { r, g, b }); this.control(id, { r, g, b });
},
async allOff() {
for (let id in this.groups) {
await this.toggleGroup(id, false);
}
} }
}, },
mounted() { mounted() {
this.fetchData(); this.fetchData();
setInterval(this.fetchData, 10000); // Раз в 10 сек для статуса ламп setInterval(this.fetchData, 5000);
} }
}).mount('#app') }).mount('#app')
</script> </script>