|
|
|
|
@ -1,5 +1,11 @@
|
|
|
|
|
<script src="assets/vendor/tailwindcss-3.4.17-cdn.js"></script>
|
|
|
|
|
<link rel="stylesheet" href="assets/vendor/fontawesome/css/all.min.css">
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="pl">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>UniView WebAdmin - Dashboard</title>
|
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
|
<script>
|
|
|
|
|
tailwind.config = {
|
|
|
|
|
darkMode: 'class',
|
|
|
|
|
@ -23,7 +29,11 @@
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
<style>
|
|
|
|
|
/* Wider, modern macOS-style scrollbar for popovers */
|
|
|
|
|
/* Ukrycie paska przewijania w menu poziomym */
|
|
|
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
|
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
|
|
|
|
|
|
|
|
/* Szerszy, nowoczesny pasek w Popoverze w stylu macOS */
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
|
|
|
width: 12px;
|
|
|
|
|
}
|
|
|
|
|
@ -32,8 +42,8 @@
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
|
|
|
background-color: rgba(156, 163, 175, 0.5); /* Semi-transparent gray */
|
|
|
|
|
border: 3px solid transparent; /* Inner border */
|
|
|
|
|
background-color: rgba(156, 163, 175, 0.5); /* Szary, półprzezroczysty */
|
|
|
|
|
border: 3px solid transparent; /* Wewnętrzne obramowanie */
|
|
|
|
|
background-clip: padding-box;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
@ -44,14 +54,77 @@
|
|
|
|
|
background-color: rgba(107, 114, 128, 0.8);
|
|
|
|
|
border-width: 2px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<div id="nscTerminalDashboard" class="flex flex-col gap-6">
|
|
|
|
|
<!-- The dashboard UI is injected here by JavaScript -->
|
|
|
|
|
</div>
|
|
|
|
|
/* Ochrona przed skakaniem interfejsu */
|
|
|
|
|
body { overflow-y: scroll; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body class="bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 transition-colors duration-300 font-sans antialiased min-h-screen flex flex-col">
|
|
|
|
|
|
|
|
|
|
<header id="layout_header" class="sticky top-0 z-[100] bg-white/90 dark:bg-slate-900/90 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 shadow-sm">
|
|
|
|
|
<div class="px-4 py-2 flex flex-col gap-2">
|
|
|
|
|
<!-- Górny Rząd: Logo, Menu Główne, Sesja -->
|
|
|
|
|
<div class="flex items-center justify-between gap-4">
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-6 overflow-hidden flex-1">
|
|
|
|
|
<!-- Logo -->
|
|
|
|
|
<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">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Pełne Menu Główne (10 pozycji) -->
|
|
|
|
|
<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-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Programy</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Kontrolery</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Lokalizacje</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Pliki</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Dane</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Raportowanie</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Usterki</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Administracja</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1.5 text-sm font-medium rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">Zaawansowane menu</a></li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Panel Użytkownika -->
|
|
|
|
|
<div class="flex items-center gap-3 flex-shrink-0">
|
|
|
|
|
<div class="hidden md:flex items-center gap-3 text-xs font-medium text-slate-500 dark:text-slate-400">
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 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">
|
|
|
|
|
<i class="fa fa-moon dark:hidden"></i>
|
|
|
|
|
<i class="fa fa-sun hidden dark:block text-amber-400"></i>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button class="flex items-center gap-2 px-3 py-1.5 text-xs font-bold text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-900 rounded-full hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors">
|
|
|
|
|
<i class="fa fa-power-off"></i> Wyloguj
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Dolny Rząd: Submenu (Tylko 2 elementy) -->
|
|
|
|
|
<ul class="flex items-center gap-2 overflow-x-auto no-scrollbar pt-1 border-t border-slate-100 dark:border-slate-800/50">
|
|
|
|
|
<li><a href="#" class="px-3 py-1 text-xs font-medium rounded-md text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">node-red</a></li>
|
|
|
|
|
<li><a href="#" class="px-3 py-1 text-xs font-semibold rounded-md bg-slate-100 dark:bg-slate-800 text-slate-800 dark:text-slate-200 shadow-sm border border-slate-200/50 dark:border-slate-700">Dashboard Sterowania</a></li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main id="layout_content" class="flex-1 p-4 md:p-6 w-full max-w-[1920px] mx-auto">
|
|
|
|
|
<div id="nscTerminalDashboard" class="flex flex-col gap-6">
|
|
|
|
|
<!-- Tutaj JS wstrzyknie interfejs Dashboardu -->
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<footer id="layout_footer" class="p-4 text-center text-xs text-slate-400 dark:text-slate-500 border-t border-slate-200 dark:border-slate-800">
|
|
|
|
|
<p>© Union Systems 2000 Sp. z o.o.</p>
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
|
|
|
<!-- Custom confirm modal replacing window.confirm -->
|
|
|
|
|
<!-- Modale Confirm (zastępuje window.confirm) -->
|
|
|
|
|
<div id="customConfirmModal" class="fixed inset-0 z-[999] flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
|
|
|
|
<div class="absolute inset-0 bg-slate-900/50 backdrop-blur-sm" id="customConfirmOverlay"></div>
|
|
|
|
|
<div class="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 w-full max-w-md p-6 transform scale-95 transition-transform duration-300" id="customConfirmBox">
|
|
|
|
|
@ -76,7 +149,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// Initialize dark mode
|
|
|
|
|
// Inicjalizacja Dark Mode
|
|
|
|
|
function initTheme() {
|
|
|
|
|
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
|
|
|
document.documentElement.classList.add('dark');
|
|
|
|
|
@ -86,7 +159,12 @@
|
|
|
|
|
}
|
|
|
|
|
initTheme();
|
|
|
|
|
|
|
|
|
|
// API and state configuration
|
|
|
|
|
document.getElementById('darkModeToggle').addEventListener('click', () => {
|
|
|
|
|
document.documentElement.classList.toggle('dark');
|
|
|
|
|
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Konfiguracja API i Stanu
|
|
|
|
|
const API_TERMINALS = "api.php?nav=api_terminal";
|
|
|
|
|
const API_COMMAND = "api.php?nav=api_run_remote_command";
|
|
|
|
|
const VISIBLE_GROUP_NAME = "Produkcja";
|
|
|
|
|
@ -94,7 +172,7 @@
|
|
|
|
|
const COMMAND_SHUTDOWN = "ShutDown";
|
|
|
|
|
const COMMAND_RESTART = "Restart";
|
|
|
|
|
|
|
|
|
|
// Application state
|
|
|
|
|
// Stan aplikacji
|
|
|
|
|
const state = {
|
|
|
|
|
terminals: [],
|
|
|
|
|
filter: "",
|
|
|
|
|
@ -110,7 +188,7 @@
|
|
|
|
|
const data = [];
|
|
|
|
|
let idCounter = 1;
|
|
|
|
|
|
|
|
|
|
// Gallery 01 mock data: 15 devices (10 PCs, 5 lighting controllers)
|
|
|
|
|
// Galeria 01 - Zgodnie z prośbą 15 urządzeń (10 PC, 5 Oświetlenie)
|
|
|
|
|
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' });
|
|
|
|
|
}
|
|
|
|
|
@ -118,7 +196,7 @@
|
|
|
|
|
data.push({ id: idCounter++, name: `CTRL-GAL01-${i}`, ip: `192.168.1.${100+i}`, status: Math.random() > 0.1 ? 'Running' : 'Stopped', location_id: 'LOC-1', location_name: 'Galeria 01', group_name: 'Produkcja', player: 'CTRL' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Galleries 02-10 mock data for grid verification
|
|
|
|
|
// Galerie 02-10 (standardowa ilość do weryfikacji siatki)
|
|
|
|
|
for (let loc = 2; loc <= 10; loc++) {
|
|
|
|
|
const locName = `Galeria ${loc.toString().padStart(2, '0')}`;
|
|
|
|
|
const locId = `LOC-${loc}`;
|
|
|
|
|
@ -132,11 +210,7 @@
|
|
|
|
|
}
|
|
|
|
|
const MOCK_DATA = generateMockData();
|
|
|
|
|
|
|
|
|
|
if (document.readyState === "loading") {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", initDashboard);
|
|
|
|
|
} else {
|
|
|
|
|
initDashboard();
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener("DOMContentLoaded", initDashboard);
|
|
|
|
|
|
|
|
|
|
function initDashboard() {
|
|
|
|
|
renderShell();
|
|
|
|
|
@ -175,13 +249,13 @@
|
|
|
|
|
if (!silent) setStatus("Ładowanie terminali...", false);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Detect preview mode where the backend API is unavailable
|
|
|
|
|
// Wykrywanie Trybu "Preview" (gdzie nie ma backendu API)
|
|
|
|
|
const isPreview = window.location.protocol === 'about:' || window.location.protocol === 'blob:' || window.location.hostname === '';
|
|
|
|
|
|
|
|
|
|
let data;
|
|
|
|
|
if (isPreview) {
|
|
|
|
|
await new Promise(r => setTimeout(r, 400)); // Simulate latency
|
|
|
|
|
data = JSON.parse(JSON.stringify(MOCK_DATA)); // MOCK data copy
|
|
|
|
|
await new Promise(r => setTimeout(r, 400)); // Symulacja opóźnienia
|
|
|
|
|
data = JSON.parse(JSON.stringify(MOCK_DATA)); // Kopia MOCK
|
|
|
|
|
} else {
|
|
|
|
|
const res = await fetch(API_TERMINALS);
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
|
|
|
|
@ -209,7 +283,7 @@
|
|
|
|
|
document.getElementById('customConfirmMessage').innerHTML = message.replace(/\n/g, '<br>');
|
|
|
|
|
|
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
|
// Short delay so the browser applies display before animation starts
|
|
|
|
|
// Małe opóźnienie dla przeglądarki, żeby zastosowała display:block przed animacją
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
modal.classList.remove('opacity-0');
|
|
|
|
|
box.classList.remove('scale-95');
|
|
|
|
|
@ -245,7 +319,7 @@
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<!-- Toolbar -->
|
|
|
|
|
<!-- Pasek Narzędzi -->
|
|
|
|
|
<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]">
|
|
|
|
|
<i class="fa fa-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
|
|
|
|
@ -268,10 +342,10 @@
|
|
|
|
|
<i class="fa fa-info-circle"></i> Inicjalizacja...
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Global summary view -->
|
|
|
|
|
<!-- Widok Globalny (Podsumowanie) -->
|
|
|
|
|
<div id="nscExhibitionCard"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Location grid -->
|
|
|
|
|
<!-- Siatka Lokalizacji -->
|
|
|
|
|
<div id="nscLocationGrid" class="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6"></div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
@ -303,7 +377,7 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDashboard() {
|
|
|
|
|
// Save the scroll position before rebuilding the open popover DOM
|
|
|
|
|
// ZAPIS: Przed przebudową DOM zapamiętujemy scroll w otwartym dymku
|
|
|
|
|
let currentScroll = 0;
|
|
|
|
|
if (state.openPopoverId) {
|
|
|
|
|
const activeScrollArea = document.querySelector(`#popover-${state.openPopoverId} .custom-scrollbar`);
|
|
|
|
|
@ -317,12 +391,11 @@
|
|
|
|
|
locationsMap.get(locId).terminals.push(t);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const allLocations = Array.from(locationsMap.values()).sort((a,b) => String(a.label).localeCompare(String(b.label), 'pl', {numeric: true}));
|
|
|
|
|
let visibleLocations = allLocations;
|
|
|
|
|
let locations = Array.from(locationsMap.values()).sort((a,b) => String(a.label).localeCompare(String(b.label), 'pl', {numeric: true}));
|
|
|
|
|
|
|
|
|
|
// Filtering
|
|
|
|
|
// Filtrowanie
|
|
|
|
|
if(state.filter) {
|
|
|
|
|
visibleLocations = allLocations.filter(loc => {
|
|
|
|
|
locations = locations.filter(loc => {
|
|
|
|
|
const matchLoc = loc.label.toLowerCase().includes(state.filter);
|
|
|
|
|
const matchTerminals = loc.terminals.some(t =>
|
|
|
|
|
(t.name || "").toLowerCase().includes(state.filter) ||
|
|
|
|
|
@ -332,44 +405,39 @@
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderGlobalExhibition(allLocations, visibleLocations);
|
|
|
|
|
renderGlobalExhibition(locations);
|
|
|
|
|
|
|
|
|
|
const grid = document.getElementById("nscLocationGrid");
|
|
|
|
|
grid.innerHTML = visibleLocations.map(renderLocationCard).join("");
|
|
|
|
|
grid.innerHTML = locations.map(renderLocationCard).join("");
|
|
|
|
|
|
|
|
|
|
// Restore the open popover and scroll position after rebuilding the HTML
|
|
|
|
|
// ODTWORZENIE: Po zbudowaniu HTML przywracamy otwarty dymek i pozycję scrolla
|
|
|
|
|
if (state.openPopoverId) {
|
|
|
|
|
const pop = document.getElementById(`popover-${state.openPopoverId}`);
|
|
|
|
|
if (pop) {
|
|
|
|
|
// Temporarily remove animation classes to avoid reanimating every 5 seconds
|
|
|
|
|
// 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.add('opacity-100', 'visible', 'scale-100');
|
|
|
|
|
|
|
|
|
|
const scrollArea = pop.querySelector('.custom-scrollbar');
|
|
|
|
|
if (scrollArea) scrollArea.scrollTop = currentScroll; // Restore scroll position
|
|
|
|
|
if (scrollArea) scrollArea.scrollTop = currentScroll; // Przywracamy przewijanie!
|
|
|
|
|
|
|
|
|
|
// Restore animations shortly after rendering
|
|
|
|
|
// Zwracamy animacje po ułamku sekundy
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
pop.classList.add('transition-all', 'duration-300');
|
|
|
|
|
}, 50);
|
|
|
|
|
} else {
|
|
|
|
|
// Clear state if filtering removed the popover from the DOM
|
|
|
|
|
// Jeśli po filtracji dymku nie ma w DOM, czyścimy stan
|
|
|
|
|
state.openPopoverId = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderGlobalExhibition(allLocations, visibleLocations) {
|
|
|
|
|
function renderGlobalExhibition(locations) {
|
|
|
|
|
const container = document.getElementById("nscExhibitionCard");
|
|
|
|
|
const allT = state.terminals;
|
|
|
|
|
const onT = allT.filter(t => t.status?.toLowerCase() === 'running').length;
|
|
|
|
|
const total = allT.length;
|
|
|
|
|
const pct = total ? Math.round((onT/total)*100) : 0;
|
|
|
|
|
const visibleLocationCount = visibleLocations.length;
|
|
|
|
|
const totalLocationCount = allLocations.length;
|
|
|
|
|
const locationSummary = state.filter
|
|
|
|
|
? `Lokalizacje: ${visibleLocationCount} / ${totalLocationCount}`
|
|
|
|
|
: `Lokalizacje: ${totalLocationCount}`;
|
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
@ -384,7 +452,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="text-xl font-bold text-slate-900 dark:text-white">Wszystkie Lokalizacje</h2>
|
|
|
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Zarządzanie całą wystawą (${locationSummary})</p>
|
|
|
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Zarządzanie całą wystawą (Galerie: ${locations.length})</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@ -423,14 +491,14 @@
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
<!-- Card header with the details button -->
|
|
|
|
|
<!-- Nagłówek Karty z nowym przyciskiem -->
|
|
|
|
|
<div class="flex justify-between items-start">
|
|
|
|
|
<div>
|
|
|
|
|
<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>
|
|
|
|
|
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1 font-mono">${loc.id}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Details button with status count -->
|
|
|
|
|
<!-- Przycisk SZCZEGÓŁY (zamiast samego statustu 11/15) -->
|
|
|
|
|
<button onclick="openPopover('${loc.id}')" class="flex items-center gap-2 px-3 py-1.5 bg-brand-50 hover:bg-brand-100 dark:bg-brand-900/20 dark:hover:bg-brand-900/40 text-brand-600 dark:text-brand-400 border border-brand-200 dark:border-brand-800/50 rounded-lg text-xs font-bold transition-colors group">
|
|
|
|
|
<span class="w-2 h-2 rounded-full ${dotCls}"></span>
|
|
|
|
|
<span>${on} / ${total}</span>
|
|
|
|
|
@ -444,7 +512,7 @@
|
|
|
|
|
<div class="h-full ${dotCls} transition-all duration-500" style="width: ${pct}%"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Controllers section on the card -->
|
|
|
|
|
<!-- Sekcja KONTROLERY na kafelku -->
|
|
|
|
|
<div class="bg-slate-50 dark:bg-slate-800/50 rounded-xl p-3 border border-slate-100 dark:border-slate-700 flex flex-col xl:flex-row justify-between items-start xl:items-center gap-3">
|
|
|
|
|
<div class="flex items-center gap-3 cursor-pointer" onclick="openPopover('${loc.id}')">
|
|
|
|
|
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 flex items-center justify-center"><i class="fa fa-desktop"></i></div>
|
|
|
|
|
@ -460,7 +528,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Lighting section on the card -->
|
|
|
|
|
<!-- Sekcja OŚWIETLENIE na kafelku -->
|
|
|
|
|
<div class="bg-slate-50 dark:bg-slate-800/50 rounded-xl p-3 border border-slate-100 dark:border-slate-700 flex flex-col xl:flex-row justify-between items-start xl:items-center gap-3">
|
|
|
|
|
<div class="flex items-center gap-3 cursor-pointer" onclick="openPopover('${loc.id}')">
|
|
|
|
|
<div class="w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 flex items-center justify-center"><i class="fa fa-lightbulb"></i></div>
|
|
|
|
|
@ -475,13 +543,13 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Popover centered over the card and matched to the card size in the grid -->
|
|
|
|
|
<!-- 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">
|
|
|
|
|
|
|
|
|
|
<!-- Popover panel filling the card frame -->
|
|
|
|
|
<!-- 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">
|
|
|
|
|
|
|
|
|
|
<!-- Popover header -->
|
|
|
|
|
<!-- 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">
|
|
|
|
|
<h4 class="font-bold text-slate-800 dark:text-slate-200 text-lg flex items-center gap-2">
|
|
|
|
|
<i class="fa fa-list text-brand-500"></i> Szczegóły: ${escapeHtml(loc.label)}
|
|
|
|
|
@ -491,7 +559,7 @@
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Scrollable area -->
|
|
|
|
|
<!-- Obszar przewijany -->
|
|
|
|
|
<div class="custom-scrollbar overflow-y-auto p-4 flex flex-col gap-4 flex-1">
|
|
|
|
|
${renderTerminalRows(pcs, 'Kontrolery (PC)', true)}
|
|
|
|
|
${renderTerminalRows(lights, 'Oświetlenie', false)}
|
|
|
|
|
@ -510,7 +578,7 @@
|
|
|
|
|
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";
|
|
|
|
|
|
|
|
|
|
// Individual action buttons
|
|
|
|
|
// Indywidualne przyciski
|
|
|
|
|
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_SHUTDOWN}', '${t.id}', '${escapeHtml(t.name)}')" class="px-2 py-1 bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600 rounded text-xs font-bold transition-colors">Wyłącz</button>
|
|
|
|
|
@ -598,18 +666,18 @@
|
|
|
|
|
setStatus(`Wysyłanie polecenia ${command} do ${targets.length} urządzeń (${scopeLabel})...`, false);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Preview mode skips FETCH requests
|
|
|
|
|
// Wykrywanie Trybu Preview wstrzymuje żądania FETCH
|
|
|
|
|
const isPreview = window.location.protocol === 'about:' || window.location.protocol === 'blob:' || window.location.hostname === '';
|
|
|
|
|
|
|
|
|
|
let failedCount = 0;
|
|
|
|
|
|
|
|
|
|
for(const t of targets) {
|
|
|
|
|
if (isPreview) {
|
|
|
|
|
await new Promise(r => setTimeout(r, 100)); // Simulate request
|
|
|
|
|
await new Promise(r => setTimeout(r, 100)); // Symulacja requestu
|
|
|
|
|
const tRef = MOCK_DATA.find(m => m.id === t.id);
|
|
|
|
|
if(tRef) tRef.status = command === COMMAND_SHUTDOWN ? 'Stopped' : 'Running';
|
|
|
|
|
} else {
|
|
|
|
|
// Production request
|
|
|
|
|
// Produkcja
|
|
|
|
|
try {
|
|
|
|
|
const body = new URLSearchParams();
|
|
|
|
|
body.set("command", command);
|
|
|
|
|
@ -636,6 +704,5 @@
|
|
|
|
|
return String(str||"").replace(/[&<>"']/g, m => ({"&":"&","<":"<",">":">",'"':""","'":"'"})[m]);
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|