|
|
|
@ -54,7 +54,7 @@
|
|
|
|
background-color: rgba(107, 114, 128, 0.8);
|
|
|
|
background-color: rgba(107, 114, 128, 0.8);
|
|
|
|
border-width: 2px;
|
|
|
|
border-width: 2px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Ochrona przed skakaniem interfejsu */
|
|
|
|
/* Ochrona przed skakaniem interfejsu */
|
|
|
|
body { overflow-y: scroll; }
|
|
|
|
body { overflow-y: scroll; }
|
|
|
|
</style>
|
|
|
|
</style>
|
|
|
|
@ -65,13 +65,13 @@
|
|
|
|
<div class="px-4 py-2 flex flex-col gap-2">
|
|
|
|
<div class="px-4 py-2 flex flex-col gap-2">
|
|
|
|
<!-- Górny Rząd: Logo, Menu Główne, Sesja -->
|
|
|
|
<!-- Górny Rząd: Logo, Menu Główne, Sesja -->
|
|
|
|
<div class="flex items-center justify-between gap-4">
|
|
|
|
<div class="flex items-center justify-between gap-4">
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-6 overflow-hidden flex-1">
|
|
|
|
<div class="flex items-center gap-6 overflow-hidden flex-1">
|
|
|
|
<!-- Logo -->
|
|
|
|
<!-- Logo -->
|
|
|
|
<div class="flex-shrink-0">
|
|
|
|
<div class="flex-shrink-0">
|
|
|
|
<img width="135" height="30" alt="UniView WebAdmin" src="https://placehold.co/135x30/2563eb/ffffff?text=UniView+Logo" class="rounded rounded-md shadow-sm">
|
|
|
|
<img width="135" height="30" alt="UniView WebAdmin" src="https://placehold.co/135x30/2563eb/ffffff?text=UniView+Logo" class="rounded rounded-md shadow-sm">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Pełne Menu Główne (10 pozycji) -->
|
|
|
|
<!-- Pełne Menu Główne (10 pozycji) -->
|
|
|
|
<ul class="flex items-center gap-1 overflow-x-auto no-scrollbar flex-1 pb-1">
|
|
|
|
<ul class="flex items-center gap-1 overflow-x-auto no-scrollbar flex-1 pb-1">
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-semibold rounded-full bg-brand-500 text-white shadow-md shadow-brand-500/20 whitespace-nowrap">Dashboard</a></li>
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-semibold rounded-full bg-brand-500 text-white shadow-md shadow-brand-500/20 whitespace-nowrap">Dashboard</a></li>
|
|
|
|
@ -93,7 +93,7 @@
|
|
|
|
<span class="px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded-md border border-slate-200 dark:border-slate-700">POLIN</span>
|
|
|
|
<span class="px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded-md border border-slate-200 dark:border-slate-700">POLIN</span>
|
|
|
|
<a href="#" class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors"><i class="fa fa-user mr-1"></i> pcs.agoral</a>
|
|
|
|
<a href="#" class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors"><i class="fa fa-user mr-1"></i> pcs.agoral</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Dark Mode Toggle -->
|
|
|
|
<!-- Dark Mode Toggle -->
|
|
|
|
<button id="darkModeToggle" class="w-8 h-8 flex items-center justify-center rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Przełącz motyw">
|
|
|
|
<button id="darkModeToggle" class="w-8 h-8 flex items-center justify-center rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Przełącz motyw">
|
|
|
|
<i class="fa fa-moon dark:hidden"></i>
|
|
|
|
<i class="fa fa-moon dark:hidden"></i>
|
|
|
|
@ -168,21 +168,17 @@
|
|
|
|
const API_TERMINALS = "api.php?nav=api_terminal";
|
|
|
|
const API_TERMINALS = "api.php?nav=api_terminal";
|
|
|
|
const API_COMMAND = "api.php?nav=api_run_remote_command";
|
|
|
|
const API_COMMAND = "api.php?nav=api_run_remote_command";
|
|
|
|
const VISIBLE_GROUP_NAME = "Produkcja";
|
|
|
|
const VISIBLE_GROUP_NAME = "Produkcja";
|
|
|
|
|
|
|
|
|
|
|
|
// GŁÓWNE KOMENDY (Zmiana: 2 nowe komendy Audio)
|
|
|
|
|
|
|
|
const COMMAND_WAKE = "WakeUp";
|
|
|
|
const COMMAND_WAKE = "WakeUp";
|
|
|
|
const COMMAND_SHUTDOWN = "ShutDown";
|
|
|
|
const COMMAND_SHUTDOWN = "ShutDown";
|
|
|
|
const COMMAND_RESTART = "Restart";
|
|
|
|
const COMMAND_RESTART = "Restart";
|
|
|
|
const COMMAND_AUDIO_ON = "AudioOn";
|
|
|
|
|
|
|
|
const COMMAND_AUDIO_OFF = "AudioOff";
|
|
|
|
// Stan aplikacji
|
|
|
|
|
|
|
|
const state = {
|
|
|
|
// Stan aplikacji
|
|
|
|
terminals: [],
|
|
|
|
const state = {
|
|
|
|
filter: "",
|
|
|
|
terminals: [],
|
|
|
|
busy: false,
|
|
|
|
filter: "",
|
|
|
|
loading: false,
|
|
|
|
busy: false,
|
|
|
|
refreshSeconds: 5,
|
|
|
|
loading: false,
|
|
|
|
|
|
|
|
refreshSeconds: 5,
|
|
|
|
|
|
|
|
refreshTimerId: null,
|
|
|
|
refreshTimerId: null,
|
|
|
|
openPopoverId: null,
|
|
|
|
openPopoverId: null,
|
|
|
|
popoverScrollTop: 0
|
|
|
|
popoverScrollTop: 0
|
|
|
|
@ -191,7 +187,7 @@
|
|
|
|
function generateMockData() {
|
|
|
|
function generateMockData() {
|
|
|
|
const data = [];
|
|
|
|
const data = [];
|
|
|
|
let idCounter = 1;
|
|
|
|
let idCounter = 1;
|
|
|
|
|
|
|
|
|
|
|
|
// Galeria 01 - Zgodnie z prośbą 15 urządzeń (10 PC, 5 Oświetlenie)
|
|
|
|
// Galeria 01 - Zgodnie z prośbą 15 urządzeń (10 PC, 5 Oświetlenie)
|
|
|
|
for(let i=1; i<=10; i++) {
|
|
|
|
for(let i=1; i<=10; i++) {
|
|
|
|
data.push({ id: idCounter++, name: `PC-GAL01-${i.toString().padStart(2, '0')}`, ip: `192.168.1.${10+i}`, status: Math.random() > 0.2 ? 'Running' : 'Stopped', location_id: 'LOC-1', location_name: 'Galeria 01', group_name: 'Produkcja', player: 'PC' });
|
|
|
|
data.push({ id: idCounter++, name: `PC-GAL01-${i.toString().padStart(2, '0')}`, ip: `192.168.1.${10+i}`, status: Math.random() > 0.2 ? 'Running' : 'Stopped', location_id: 'LOC-1', location_name: 'Galeria 01', group_name: 'Produkcja', player: 'PC' });
|
|
|
|
@ -204,7 +200,7 @@
|
|
|
|
for (let loc = 2; loc <= 10; loc++) {
|
|
|
|
for (let loc = 2; loc <= 10; loc++) {
|
|
|
|
const locName = `Galeria ${loc.toString().padStart(2, '0')}`;
|
|
|
|
const locName = `Galeria ${loc.toString().padStart(2, '0')}`;
|
|
|
|
const locId = `LOC-${loc}`;
|
|
|
|
const locId = `LOC-${loc}`;
|
|
|
|
|
|
|
|
|
|
|
|
for(let i=1; i<=2; i++) {
|
|
|
|
for(let i=1; i<=2; i++) {
|
|
|
|
data.push({ id: idCounter++, name: `PC-GAL${loc}-${i}`, ip: `192.168.${loc}.${i}`, status: Math.random() > 0.3 ? 'Running' : 'Stopped', location_id: locId, location_name: locName, group_name: 'Produkcja', player: 'PC' });
|
|
|
|
data.push({ id: idCounter++, name: `PC-GAL${loc}-${i}`, ip: `192.168.${loc}.${i}`, status: Math.random() > 0.3 ? 'Running' : 'Stopped', location_id: locId, location_name: locName, group_name: 'Produkcja', player: 'PC' });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -255,7 +251,7 @@
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Wykrywanie Trybu "Preview" (gdzie nie ma backendu API)
|
|
|
|
// Wykrywanie Trybu "Preview" (gdzie nie ma backendu API)
|
|
|
|
const isPreview = window.location.protocol === 'about:' || window.location.protocol === 'blob:' || window.location.hostname === '';
|
|
|
|
const isPreview = window.location.protocol === 'about:' || window.location.protocol === 'blob:' || window.location.hostname === '';
|
|
|
|
|
|
|
|
|
|
|
|
let data;
|
|
|
|
let data;
|
|
|
|
if (isPreview) {
|
|
|
|
if (isPreview) {
|
|
|
|
await new Promise(r => setTimeout(r, 400)); // Symulacja opóźnienia
|
|
|
|
await new Promise(r => setTimeout(r, 400)); // Symulacja opóźnienia
|
|
|
|
@ -282,11 +278,12 @@
|
|
|
|
const modal = document.getElementById('customConfirmModal');
|
|
|
|
const modal = document.getElementById('customConfirmModal');
|
|
|
|
const box = document.getElementById('customConfirmBox');
|
|
|
|
const box = document.getElementById('customConfirmBox');
|
|
|
|
const overlay = document.getElementById('customConfirmOverlay');
|
|
|
|
const overlay = document.getElementById('customConfirmOverlay');
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('customConfirmTitle').textContent = title;
|
|
|
|
document.getElementById('customConfirmTitle').textContent = title;
|
|
|
|
document.getElementById('customConfirmMessage').innerHTML = message.replace(/\n/g, '<br>');
|
|
|
|
document.getElementById('customConfirmMessage').innerHTML = message.replace(/\n/g, '<br>');
|
|
|
|
|
|
|
|
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
|
|
|
|
// Małe opóźnienie dla przeglądarki, żeby zastosowała display:block przed animacją
|
|
|
|
setTimeout(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
modal.classList.remove('opacity-0');
|
|
|
|
modal.classList.remove('opacity-0');
|
|
|
|
box.classList.remove('scale-95');
|
|
|
|
box.classList.remove('scale-95');
|
|
|
|
@ -298,7 +295,7 @@
|
|
|
|
box.classList.remove('scale-100');
|
|
|
|
box.classList.remove('scale-100');
|
|
|
|
box.classList.add('scale-95');
|
|
|
|
box.classList.add('scale-95');
|
|
|
|
setTimeout(() => modal.classList.add('hidden'), 300);
|
|
|
|
setTimeout(() => modal.classList.add('hidden'), 300);
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('customConfirmCancel').onclick = null;
|
|
|
|
document.getElementById('customConfirmCancel').onclick = null;
|
|
|
|
document.getElementById('customConfirmOk').onclick = null;
|
|
|
|
document.getElementById('customConfirmOk').onclick = null;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -321,7 +318,7 @@
|
|
|
|
<h1 class="text-2xl md:text-3xl font-extrabold text-slate-900 dark:text-white tracking-tight">Sterowanie Wystawą</h1>
|
|
|
|
<h1 class="text-2xl md:text-3xl font-extrabold text-slate-900 dark:text-white tracking-tight">Sterowanie Wystawą</h1>
|
|
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Grupowe oraz punktowe zarządzanie zasilaniem urządzeń. Grupa: <strong>${VISIBLE_GROUP_NAME}</strong></p>
|
|
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Grupowe oraz punktowe zarządzanie zasilaniem urządzeń. Grupa: <strong>${VISIBLE_GROUP_NAME}</strong></p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Pasek Narzędzi -->
|
|
|
|
<!-- Pasek Narzędzi -->
|
|
|
|
<div class="flex flex-wrap items-center gap-3 w-full xl:w-auto">
|
|
|
|
<div class="flex flex-wrap items-center gap-3 w-full xl:w-auto">
|
|
|
|
<div class="relative flex-1 xl:w-64 min-w-[200px]">
|
|
|
|
<div class="relative flex-1 xl:w-64 min-w-[200px]">
|
|
|
|
@ -369,7 +366,7 @@
|
|
|
|
pop.classList.add('opacity-100', 'visible', 'scale-100');
|
|
|
|
pop.classList.add('opacity-100', 'visible', 'scale-100');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closePopover(locId) {
|
|
|
|
function closePopover(locId) {
|
|
|
|
if(state.openPopoverId === locId) state.openPopoverId = null;
|
|
|
|
if(state.openPopoverId === locId) state.openPopoverId = null;
|
|
|
|
const pop = document.getElementById(`popover-${locId}`);
|
|
|
|
const pop = document.getElementById(`popover-${locId}`);
|
|
|
|
@ -393,15 +390,15 @@
|
|
|
|
if (!locationsMap.has(locId)) locationsMap.set(locId, { id: locId, label: t.location_name || locId, terminals: [] });
|
|
|
|
if (!locationsMap.has(locId)) locationsMap.set(locId, { id: locId, label: t.location_name || locId, terminals: [] });
|
|
|
|
locationsMap.get(locId).terminals.push(t);
|
|
|
|
locationsMap.get(locId).terminals.push(t);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let locations = Array.from(locationsMap.values()).sort((a,b) => String(a.label).localeCompare(String(b.label), 'pl', {numeric: true}));
|
|
|
|
let locations = Array.from(locationsMap.values()).sort((a,b) => String(a.label).localeCompare(String(b.label), 'pl', {numeric: true}));
|
|
|
|
|
|
|
|
|
|
|
|
// Filtrowanie
|
|
|
|
// Filtrowanie
|
|
|
|
if(state.filter) {
|
|
|
|
if(state.filter) {
|
|
|
|
locations = locations.filter(loc => {
|
|
|
|
locations = locations.filter(loc => {
|
|
|
|
const matchLoc = loc.label.toLowerCase().includes(state.filter);
|
|
|
|
const matchLoc = loc.label.toLowerCase().includes(state.filter);
|
|
|
|
const matchTerminals = loc.terminals.some(t =>
|
|
|
|
const matchTerminals = loc.terminals.some(t =>
|
|
|
|
(t.name || "").toLowerCase().includes(state.filter) ||
|
|
|
|
(t.name || "").toLowerCase().includes(state.filter) ||
|
|
|
|
(t.ip || "").includes(state.filter)
|
|
|
|
(t.ip || "").includes(state.filter)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
return matchLoc || matchTerminals;
|
|
|
|
return matchLoc || matchTerminals;
|
|
|
|
@ -409,7 +406,7 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderGlobalExhibition(locations);
|
|
|
|
renderGlobalExhibition(locations);
|
|
|
|
|
|
|
|
|
|
|
|
const grid = document.getElementById("nscLocationGrid");
|
|
|
|
const grid = document.getElementById("nscLocationGrid");
|
|
|
|
grid.innerHTML = locations.map(renderLocationCard).join("");
|
|
|
|
grid.innerHTML = locations.map(renderLocationCard).join("");
|
|
|
|
|
|
|
|
|
|
|
|
@ -420,7 +417,7 @@
|
|
|
|
// Usuwamy klasy animacji tymczasowo, by nie "wjeżdżał" co 5 sekund
|
|
|
|
// Usuwamy klasy animacji tymczasowo, by nie "wjeżdżał" co 5 sekund
|
|
|
|
pop.classList.remove('transition-all', 'duration-300', 'opacity-0', 'invisible', 'scale-95', 'pointer-events-none');
|
|
|
|
pop.classList.remove('transition-all', 'duration-300', 'opacity-0', 'invisible', 'scale-95', 'pointer-events-none');
|
|
|
|
pop.classList.add('opacity-100', 'visible', 'scale-100');
|
|
|
|
pop.classList.add('opacity-100', 'visible', 'scale-100');
|
|
|
|
|
|
|
|
|
|
|
|
const scrollArea = pop.querySelector('.custom-scrollbar');
|
|
|
|
const scrollArea = pop.querySelector('.custom-scrollbar');
|
|
|
|
if (scrollArea) scrollArea.scrollTop = currentScroll; // Przywracamy przewijanie!
|
|
|
|
if (scrollArea) scrollArea.scrollTop = currentScroll; // Przywracamy przewijanie!
|
|
|
|
|
|
|
|
|
|
|
|
@ -441,7 +438,7 @@
|
|
|
|
const onT = allT.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
const onT = allT.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
const total = allT.length;
|
|
|
|
const total = allT.length;
|
|
|
|
const pct = total ? Math.round((onT/total)*100) : 0;
|
|
|
|
const pct = total ? Math.round((onT/total)*100) : 0;
|
|
|
|
|
|
|
|
|
|
|
|
let colorCls = "bg-slate-100 text-slate-500 border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700";
|
|
|
|
let colorCls = "bg-slate-100 text-slate-500 border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700";
|
|
|
|
let fillCls = "bg-slate-400";
|
|
|
|
let fillCls = "bg-slate-400";
|
|
|
|
if(total > 0 && onT === total) { colorCls = "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800/50"; fillCls = "bg-emerald-500"; }
|
|
|
|
if(total > 0 && onT === total) { colorCls = "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800/50"; fillCls = "bg-emerald-500"; }
|
|
|
|
@ -458,7 +455,7 @@
|
|
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Zarządzanie całą wystawą (Galerie: ${locations.length})</p>
|
|
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Zarządzanie całą wystawą (Galerie: ${locations.length})</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex-1 max-w-md w-full">
|
|
|
|
<div class="flex-1 max-w-md w-full">
|
|
|
|
<div class="flex justify-between text-xs font-bold mb-2">
|
|
|
|
<div class="flex justify-between text-xs font-bold mb-2">
|
|
|
|
<span class="text-slate-500 dark:text-slate-400 uppercase tracking-wider">Uruchomione</span>
|
|
|
|
<span class="text-slate-500 dark:text-slate-400 uppercase tracking-wider">Uruchomione</span>
|
|
|
|
@ -481,10 +478,10 @@
|
|
|
|
const total = loc.terminals.length;
|
|
|
|
const total = loc.terminals.length;
|
|
|
|
const on = loc.terminals.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
const on = loc.terminals.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
const pct = total ? Math.round((on/total)*100) : 0;
|
|
|
|
const pct = total ? Math.round((on/total)*100) : 0;
|
|
|
|
|
|
|
|
|
|
|
|
const pcs = loc.terminals.filter(t => t.player === 'PC');
|
|
|
|
const pcs = loc.terminals.filter(t => t.player === 'PC');
|
|
|
|
const onPcs = pcs.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
const onPcs = pcs.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
|
|
|
|
|
|
|
|
const lights = loc.terminals.filter(t => t.player === 'CTRL');
|
|
|
|
const lights = loc.terminals.filter(t => t.player === 'CTRL');
|
|
|
|
const onLights = lights.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
const onLights = lights.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
|
|
|
|
|
|
|
|
@ -494,17 +491,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
return `
|
|
|
|
<div class="relative bg-white dark:bg-slate-800 rounded-2xl p-5 shadow-soft border border-slate-200 dark:border-slate-700 flex flex-col gap-4">
|
|
|
|
<div class="relative bg-white dark:bg-slate-800 rounded-2xl p-5 shadow-soft border border-slate-200 dark:border-slate-700 flex flex-col gap-4">
|
|
|
|
|
|
|
|
<!-- Nagłówek Karty (Elastyczny pasek) -->
|
|
|
|
<!-- Nagłówek Karty z wbudowanym panelem przycisków -->
|
|
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
|
|
|
|
<div class="flex flex-col sm:flex-row justify-between items-start gap-3 w-full">
|
|
|
|
<!-- Lewa strona: Nazwa galerii -->
|
|
|
|
<!-- Lewa strona: Nazwa galerii (Brak ID!) -->
|
|
|
|
<div>
|
|
|
|
<div class="flex-shrink-0">
|
|
|
|
<h3 class="text-xl font-bold text-slate-900 dark:text-white cursor-pointer hover:text-brand-600 dark:hover:text-brand-400 transition-colors" onclick="openPopover('${loc.id}')">${escapeHtml(loc.label)}</h3>
|
|
|
|
<h3 class="text-xl font-bold text-slate-900 dark:text-white cursor-pointer hover:text-brand-600 dark:hover:text-brand-400 transition-colors whitespace-nowrap" onclick="openPopover('${loc.id}')">${escapeHtml(loc.label)}</h3>
|
|
|
|
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1 font-mono">${loc.id}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Prawa strona: 4 Kontrolki + Przycisk SZCZEGÓŁY -->
|
|
|
|
<!-- Prawa strona: Kontrolki Galerii + Przycisk SZCZEGÓŁY -->
|
|
|
|
<div class="flex items-center justify-end gap-2 sm:gap-3 flex-1 flex-wrap">
|
|
|
|
<div class="flex items-center gap-3 self-stretch sm:self-auto flex-wrap">
|
|
|
|
<!-- Przyciski całej galerii (Włącz/Wyłącz/Audio ON/Audio OFF) -->
|
|
|
|
<!-- Przyciski całej galerii (Włącz/Wyłącz/Wycisz) -->
|
|
|
|
<div class="flex bg-slate-100 dark:bg-slate-800 p-1.5 rounded-xl border border-slate-200 dark:border-slate-700 shadow-inner gap-1">
|
|
|
|
<div class="flex bg-slate-100 dark:bg-slate-800 p-1.5 rounded-xl border border-slate-200 dark:border-slate-700 shadow-inner gap-1">
|
|
|
|
<button onclick="runCommand('${COMMAND_WAKE}', '${loc.id}', 'all')" class="px-3 py-1.5 text-emerald-600 hover:bg-white dark:text-emerald-400 dark:hover:bg-slate-700 hover:shadow-sm rounded-lg text-sm font-bold transition-all flex items-center gap-2" title="Włącz całą galerię">
|
|
|
|
<button onclick="runCommand('${COMMAND_WAKE}', '${loc.id}', 'all')" class="px-3 py-1.5 text-emerald-600 hover:bg-white dark:text-emerald-400 dark:hover:bg-slate-700 hover:shadow-sm rounded-lg text-sm font-bold transition-all flex items-center gap-2" title="Włącz całą galerię">
|
|
|
|
<i class="fa fa-play"></i> <span class="hidden xl:inline">Włącz</span>
|
|
|
|
<i class="fa fa-play"></i> <span class="hidden xl:inline">Włącz</span>
|
|
|
|
@ -512,11 +509,8 @@
|
|
|
|
<button onclick="runCommand('${COMMAND_SHUTDOWN}', '${loc.id}', 'all')" class="px-3 py-1.5 text-slate-600 hover:bg-white dark:text-slate-300 dark:hover:bg-slate-700 hover:shadow-sm rounded-lg text-sm font-bold transition-all flex items-center gap-2" title="Wyłącz całą galerię">
|
|
|
|
<button onclick="runCommand('${COMMAND_SHUTDOWN}', '${loc.id}', 'all')" class="px-3 py-1.5 text-slate-600 hover:bg-white dark:text-slate-300 dark:hover:bg-slate-700 hover:shadow-sm rounded-lg text-sm font-bold transition-all flex items-center gap-2" title="Wyłącz całą galerię">
|
|
|
|
<i class="fa fa-stop"></i> <span class="hidden xl:inline">Wyłącz</span>
|
|
|
|
<i class="fa fa-stop"></i> <span class="hidden xl:inline">Wyłącz</span>
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button onclick="runCommand('${COMMAND_AUDIO_ON}', '${loc.id}', 'all')" class="px-3 py-1.5 text-sky-600 hover:bg-white dark:text-sky-400 dark:hover:bg-slate-700 hover:shadow-sm rounded-lg text-sm font-bold transition-all flex items-center gap-2" title="Włącz Audio w galerii">
|
|
|
|
<button onclick="runCommand('${COMMAND_MUTE}', '${loc.id}', 'all')" class="px-3 py-1.5 text-amber-600 hover:bg-white dark:text-amber-400 dark:hover:bg-slate-700 hover:shadow-sm rounded-lg text-sm font-bold transition-all flex items-center gap-2" title="Wycisz całą galerię">
|
|
|
|
<i class="fa fa-volume-up"></i> <span class="hidden xl:inline">Audio ON</span>
|
|
|
|
<i class="fa fa-volume-mute"></i> <span class="hidden xl:inline">Wycisz</span>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button onclick="runCommand('${COMMAND_AUDIO_OFF}', '${loc.id}', 'all')" class="px-3 py-1.5 text-amber-600 hover:bg-white dark:text-amber-400 dark:hover:bg-slate-700 hover:shadow-sm rounded-lg text-sm font-bold transition-all flex items-center gap-2" title="Wyłącz Audio w galerii">
|
|
|
|
|
|
|
|
<i class="fa fa-volume-mute"></i> <span class="hidden xl:inline">Audio OFF</span>
|
|
|
|
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
@ -566,12 +560,12 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Dymek (Popover) centralnie nad kafelkiem -->
|
|
|
|
<!-- Dymek (Popover) centralnie nad kafelkiem, wymiar w 100% dopasowany do kafelka w ramach siatki -->
|
|
|
|
<div id="popover-${loc.id}" class="absolute inset-0 z-50 flex opacity-0 invisible scale-95 transition-all duration-300 pointer-events-none">
|
|
|
|
<div id="popover-${loc.id}" class="absolute inset-0 z-50 flex opacity-0 invisible scale-95 transition-all duration-300 pointer-events-none">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Właściwe okienko (zajmujące 100% obszaru) -->
|
|
|
|
<!-- Właściwe okienko (zajmujące calutką ramkę) -->
|
|
|
|
<div class="relative w-full h-full flex flex-col bg-white dark:bg-slate-800 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 pointer-events-auto overflow-hidden">
|
|
|
|
<div class="relative w-full h-full flex flex-col bg-white dark:bg-slate-800 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 pointer-events-auto overflow-hidden">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Header Dymka -->
|
|
|
|
<!-- Header Dymka -->
|
|
|
|
<div class="flex items-center justify-between p-4 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">
|
|
|
|
<div class="flex items-center justify-between p-4 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">
|
|
|
|
<h4 class="font-bold text-slate-800 dark:text-slate-200 text-lg flex items-center gap-2">
|
|
|
|
<h4 class="font-bold text-slate-800 dark:text-slate-200 text-lg flex items-center gap-2">
|
|
|
|
@ -581,7 +575,7 @@
|
|
|
|
<i class="fa fa-times"></i>
|
|
|
|
<i class="fa fa-times"></i>
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Obszar przewijany -->
|
|
|
|
<!-- Obszar przewijany -->
|
|
|
|
<div class="custom-scrollbar overflow-y-auto p-4 flex flex-col gap-4 flex-1">
|
|
|
|
<div class="custom-scrollbar overflow-y-auto p-4 flex flex-col gap-4 flex-1">
|
|
|
|
${renderTerminalRows(pcs, 'Kontrolery (PC)', true)}
|
|
|
|
${renderTerminalRows(pcs, 'Kontrolery (PC)', true)}
|
|
|
|
@ -595,12 +589,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
function renderTerminalRows(terminals, title, allowRestart) {
|
|
|
|
function renderTerminalRows(terminals, title, allowRestart) {
|
|
|
|
if(!terminals.length) return '';
|
|
|
|
if(!terminals.length) return '';
|
|
|
|
|
|
|
|
|
|
|
|
const rows = terminals.map(t => {
|
|
|
|
const rows = terminals.map(t => {
|
|
|
|
const isRun = t.status?.toLowerCase() === 'running';
|
|
|
|
const isRun = t.status?.toLowerCase() === 'running';
|
|
|
|
const statusCls = isRun ? "text-emerald-500" : "text-slate-400 dark:text-slate-500";
|
|
|
|
const statusCls = isRun ? "text-emerald-500" : "text-slate-400 dark:text-slate-500";
|
|
|
|
const dotCls = isRun ? "bg-emerald-500" : "bg-slate-300 dark:bg-slate-600";
|
|
|
|
const dotCls = isRun ? "bg-emerald-500" : "bg-slate-300 dark:bg-slate-600";
|
|
|
|
|
|
|
|
|
|
|
|
// Indywidualne przyciski
|
|
|
|
// Indywidualne przyciski
|
|
|
|
let actions = `
|
|
|
|
let actions = `
|
|
|
|
<button onclick="runSingleCommand('${COMMAND_WAKE}', '${t.id}', '${escapeHtml(t.name)}')" class="px-2 py-1 bg-emerald-50 text-emerald-600 hover:bg-emerald-100 dark:bg-emerald-900/20 dark:text-emerald-400 dark:hover:bg-emerald-900/40 rounded text-xs font-bold transition-colors">Włącz</button>
|
|
|
|
<button onclick="runSingleCommand('${COMMAND_WAKE}', '${t.id}', '${escapeHtml(t.name)}')" class="px-2 py-1 bg-emerald-50 text-emerald-600 hover:bg-emerald-100 dark:bg-emerald-900/20 dark:text-emerald-400 dark:hover:bg-emerald-900/40 rounded text-xs font-bold transition-colors">Włącz</button>
|
|
|
|
@ -649,17 +643,10 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runCommand(command, locationId, type) {
|
|
|
|
async function runCommand(command, locationId, type) {
|
|
|
|
const locName = locationId === 'all' ? "Cała wystawa" : (state.terminals.find(t => t.location_id === locationId)?.location_name || locationId);
|
|
|
|
const locName = state.terminals.find(t => t.location_id === locationId)?.location_name || locationId;
|
|
|
|
const typeName = type === 'computer' ? 'Kontrolery' : (type === 'ctrl' ? 'Oświetlenie' : 'Wszystkie urządzenia');
|
|
|
|
const typeName = type === 'computer' ? 'Kontrolery' : 'Oświetlenie';
|
|
|
|
|
|
|
|
const actionName = command === COMMAND_WAKE ? "Włączenie" : (command === COMMAND_SHUTDOWN ? "Wyłączenie" : "Restart");
|
|
|
|
// Logika nazw dla nowych komend
|
|
|
|
|
|
|
|
let actionName = "";
|
|
|
|
|
|
|
|
if(command === COMMAND_WAKE) actionName = "Włączenie";
|
|
|
|
|
|
|
|
else if(command === COMMAND_SHUTDOWN) actionName = "Wyłączenie";
|
|
|
|
|
|
|
|
else if(command === COMMAND_RESTART) actionName = "Restart";
|
|
|
|
|
|
|
|
else if(command === COMMAND_AUDIO_ON) actionName = "Włączenie Audio";
|
|
|
|
|
|
|
|
else if(command === COMMAND_AUDIO_OFF) actionName = "Wyłączenie Audio";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const confirmed = await customConfirm(
|
|
|
|
const confirmed = await customConfirm(
|
|
|
|
`Akcja grupowa: ${actionName}`,
|
|
|
|
`Akcja grupowa: ${actionName}`,
|
|
|
|
`Lokalizacja: <strong>${locName}</strong>\nUrządzenia: <strong>${typeName}</strong>\n\nCzy na pewno chcesz kontynuować?`
|
|
|
|
`Lokalizacja: <strong>${locName}</strong>\nUrządzenia: <strong>${typeName}</strong>\n\nCzy na pewno chcesz kontynuować?`
|
|
|
|
@ -668,20 +655,14 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runSingleCommand(command, terminalId, terminalName) {
|
|
|
|
async function runSingleCommand(command, terminalId, terminalName) {
|
|
|
|
let actionName = "";
|
|
|
|
const actionName = command === COMMAND_WAKE ? "Włączenie" : (command === COMMAND_SHUTDOWN ? "Wyłączenie" : "Restart");
|
|
|
|
if(command === COMMAND_WAKE) actionName = "Włączenie";
|
|
|
|
|
|
|
|
else if(command === COMMAND_SHUTDOWN) actionName = "Wyłączenie";
|
|
|
|
|
|
|
|
else if(command === COMMAND_RESTART) actionName = "Restart";
|
|
|
|
|
|
|
|
else if(command === COMMAND_AUDIO_ON) actionName = "Włączenie Audio";
|
|
|
|
|
|
|
|
else if(command === COMMAND_AUDIO_OFF) actionName = "Wyłączenie Audio";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const terminal = state.terminals.find(t => String(t.id) === String(terminalId));
|
|
|
|
const terminal = state.terminals.find(t => String(t.id) === String(terminalId));
|
|
|
|
|
|
|
|
|
|
|
|
const confirmed = await customConfirm(
|
|
|
|
const confirmed = await customConfirm(
|
|
|
|
`Pojedyncza Akcja: ${actionName}`,
|
|
|
|
`Pojedyncza Akcja: ${actionName}`,
|
|
|
|
`Urządzenie: <strong>${terminalName}</strong>\nIP: ${terminal?.ip || 'Brak IP'}\n\nCzy na pewno wysłać polecenie?`
|
|
|
|
`Urządzenie: <strong>${terminalName}</strong>\nIP: ${terminal?.ip || 'Brak IP'}\n\nCzy na pewno wysłać polecenie?`
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if(confirmed) {
|
|
|
|
if(confirmed) {
|
|
|
|
if(terminal) executeCommandBulk(command, [terminal], terminalName);
|
|
|
|
if(terminal) executeCommandBulk(command, [terminal], terminalName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -700,11 +681,11 @@
|
|
|
|
async function executeCommandBulk(command, targets, scopeLabel) {
|
|
|
|
async function executeCommandBulk(command, targets, scopeLabel) {
|
|
|
|
state.busy = true;
|
|
|
|
state.busy = true;
|
|
|
|
setStatus(`Wysyłanie polecenia ${command} do ${targets.length} urządzeń (${scopeLabel})...`, false);
|
|
|
|
setStatus(`Wysyłanie polecenia ${command} do ${targets.length} urządzeń (${scopeLabel})...`, false);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Wykrywanie Trybu Preview wstrzymuje żądania FETCH
|
|
|
|
// Wykrywanie Trybu Preview wstrzymuje żądania FETCH
|
|
|
|
const isPreview = window.location.protocol === 'about:' || window.location.protocol === 'blob:' || window.location.hostname === '';
|
|
|
|
const isPreview = window.location.protocol === 'about:' || window.location.protocol === 'blob:' || window.location.hostname === '';
|
|
|
|
|
|
|
|
|
|
|
|
let failedCount = 0;
|
|
|
|
let failedCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for(const t of targets) {
|
|
|
|
for(const t of targets) {
|
|
|
|
@ -718,7 +699,7 @@
|
|
|
|
const body = new URLSearchParams();
|
|
|
|
const body = new URLSearchParams();
|
|
|
|
body.set("command", command);
|
|
|
|
body.set("command", command);
|
|
|
|
body.set("tag_id", t.id);
|
|
|
|
body.set("tag_id", t.id);
|
|
|
|
|
|
|
|
|
|
|
|
const res = await fetch(API_COMMAND, { method: "POST", body: body });
|
|
|
|
const res = await fetch(API_COMMAND, { method: "POST", body: body });
|
|
|
|
if(!res.ok) failedCount++;
|
|
|
|
if(!res.ok) failedCount++;
|
|
|
|
} catch(e) {
|
|
|
|
} catch(e) {
|
|
|
|
@ -726,10 +707,10 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if(failedCount > 0) setStatus(`Zakończono z błędami. Niepowodzenia: ${failedCount}/${targets.length}`, true);
|
|
|
|
if(failedCount > 0) setStatus(`Zakończono z błędami. Niepowodzenia: ${failedCount}/${targets.length}`, true);
|
|
|
|
else setStatus(`Polecenie wykonane pomyślnie dla ${targets.length} urządzeń.`, false);
|
|
|
|
else setStatus(`Polecenie wykonane pomyślnie dla ${targets.length} urządzeń.`, false);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => refreshTerminals(true), 1500);
|
|
|
|
setTimeout(() => refreshTerminals(true), 1500);
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
state.busy = false;
|
|
|
|
state.busy = false;
|
|
|
|
@ -741,4 +722,4 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
</html>
|
|
|
|
|