Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa79b27158 | |||
| dc8fa9b44e |
+9
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+83
File diff suppressed because one or more lines are too long
@@ -1,11 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<script src="assets/vendor/tailwindcss-3.4.17-cdn.js"></script>
|
||||||
<html lang="pl">
|
<link rel="stylesheet" href="assets/vendor/fontawesome/css/all.min.css">
|
||||||
<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>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
@@ -29,11 +23,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
/* Ukrycie paska przewijania w menu poziomym */
|
/* Wider, modern macOS-style scrollbar for popovers */
|
||||||
.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 {
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
@@ -42,8 +32,8 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(156, 163, 175, 0.5); /* Szary, półprzezroczysty */
|
background-color: rgba(156, 163, 175, 0.5); /* Semi-transparent gray */
|
||||||
border: 3px solid transparent; /* Wewnętrzne obramowanie */
|
border: 3px solid transparent; /* Inner border */
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
@@ -54,77 +44,14 @@
|
|||||||
background-color: rgba(107, 114, 128, 0.8);
|
background-color: rgba(107, 114, 128, 0.8);
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
/* Ochrona przed skakaniem interfejsu */
|
<div id="nscTerminalDashboard" class="flex flex-col gap-6">
|
||||||
body { overflow-y: scroll; }
|
<!-- The dashboard UI is injected here by JavaScript -->
|
||||||
</style>
|
</div>
|
||||||
</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">
|
<!-- Custom confirm modal replacing window.confirm -->
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- 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 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="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">
|
<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">
|
||||||
@@ -149,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Inicjalizacja Dark Mode
|
// Initialize dark mode
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
@@ -159,12 +86,7 @@
|
|||||||
}
|
}
|
||||||
initTheme();
|
initTheme();
|
||||||
|
|
||||||
document.getElementById('darkModeToggle').addEventListener('click', () => {
|
// API and state configuration
|
||||||
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_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";
|
||||||
@@ -172,7 +94,7 @@
|
|||||||
const COMMAND_SHUTDOWN = "ShutDown";
|
const COMMAND_SHUTDOWN = "ShutDown";
|
||||||
const COMMAND_RESTART = "Restart";
|
const COMMAND_RESTART = "Restart";
|
||||||
|
|
||||||
// Stan aplikacji
|
// Application state
|
||||||
const state = {
|
const state = {
|
||||||
terminals: [],
|
terminals: [],
|
||||||
filter: "",
|
filter: "",
|
||||||
@@ -188,7 +110,7 @@
|
|||||||
const data = [];
|
const data = [];
|
||||||
let idCounter = 1;
|
let idCounter = 1;
|
||||||
|
|
||||||
// Galeria 01 - Zgodnie z prośbą 15 urządzeń (10 PC, 5 Oświetlenie)
|
// Gallery 01 mock data: 15 devices (10 PCs, 5 lighting controllers)
|
||||||
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' });
|
||||||
}
|
}
|
||||||
@@ -196,7 +118,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' });
|
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' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Galerie 02-10 (standardowa ilość do weryfikacji siatki)
|
// Galleries 02-10 mock data for grid verification
|
||||||
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}`;
|
||||||
@@ -210,7 +132,11 @@
|
|||||||
}
|
}
|
||||||
const MOCK_DATA = generateMockData();
|
const MOCK_DATA = generateMockData();
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", initDashboard);
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", initDashboard);
|
||||||
|
} else {
|
||||||
|
initDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
function initDashboard() {
|
function initDashboard() {
|
||||||
renderShell();
|
renderShell();
|
||||||
@@ -249,13 +175,13 @@
|
|||||||
if (!silent) setStatus("Ładowanie terminali...", false);
|
if (!silent) setStatus("Ładowanie terminali...", false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wykrywanie Trybu "Preview" (gdzie nie ma backendu API)
|
// Detect preview mode where the backend API is unavailable
|
||||||
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)); // Simulate latency
|
||||||
data = JSON.parse(JSON.stringify(MOCK_DATA)); // Kopia MOCK
|
data = JSON.parse(JSON.stringify(MOCK_DATA)); // MOCK data copy
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(API_TERMINALS);
|
const res = await fetch(API_TERMINALS);
|
||||||
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
||||||
@@ -283,7 +209,7 @@
|
|||||||
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ą
|
// Short delay so the browser applies display before animation starts
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.classList.remove('opacity-0');
|
modal.classList.remove('opacity-0');
|
||||||
box.classList.remove('scale-95');
|
box.classList.remove('scale-95');
|
||||||
@@ -319,7 +245,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>
|
<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 -->
|
<!-- Toolbar -->
|
||||||
<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]">
|
||||||
<i class="fa fa-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
<i class="fa fa-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||||
@@ -342,10 +268,10 @@
|
|||||||
<i class="fa fa-info-circle"></i> Inicjalizacja...
|
<i class="fa fa-info-circle"></i> Inicjalizacja...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Widok Globalny (Podsumowanie) -->
|
<!-- Global summary view -->
|
||||||
<div id="nscExhibitionCard"></div>
|
<div id="nscExhibitionCard"></div>
|
||||||
|
|
||||||
<!-- Siatka Lokalizacji -->
|
<!-- Location grid -->
|
||||||
<div id="nscLocationGrid" class="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6"></div>
|
<div id="nscLocationGrid" class="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6"></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -377,7 +303,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDashboard() {
|
function renderDashboard() {
|
||||||
// ZAPIS: Przed przebudową DOM zapamiętujemy scroll w otwartym dymku
|
// Save the scroll position before rebuilding the open popover DOM
|
||||||
let currentScroll = 0;
|
let currentScroll = 0;
|
||||||
if (state.openPopoverId) {
|
if (state.openPopoverId) {
|
||||||
const activeScrollArea = document.querySelector(`#popover-${state.openPopoverId} .custom-scrollbar`);
|
const activeScrollArea = document.querySelector(`#popover-${state.openPopoverId} .custom-scrollbar`);
|
||||||
@@ -391,11 +317,12 @@
|
|||||||
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}));
|
const allLocations = Array.from(locationsMap.values()).sort((a,b) => String(a.label).localeCompare(String(b.label), 'pl', {numeric: true}));
|
||||||
|
let visibleLocations = allLocations;
|
||||||
|
|
||||||
// Filtrowanie
|
// Filtering
|
||||||
if(state.filter) {
|
if(state.filter) {
|
||||||
locations = locations.filter(loc => {
|
visibleLocations = allLocations.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) ||
|
||||||
@@ -405,39 +332,44 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGlobalExhibition(locations);
|
renderGlobalExhibition(allLocations, visibleLocations);
|
||||||
|
|
||||||
const grid = document.getElementById("nscLocationGrid");
|
const grid = document.getElementById("nscLocationGrid");
|
||||||
grid.innerHTML = locations.map(renderLocationCard).join("");
|
grid.innerHTML = visibleLocations.map(renderLocationCard).join("");
|
||||||
|
|
||||||
// ODTWORZENIE: Po zbudowaniu HTML przywracamy otwarty dymek i pozycję scrolla
|
// Restore the open popover and scroll position after rebuilding the HTML
|
||||||
if (state.openPopoverId) {
|
if (state.openPopoverId) {
|
||||||
const pop = document.getElementById(`popover-${state.openPopoverId}`);
|
const pop = document.getElementById(`popover-${state.openPopoverId}`);
|
||||||
if (pop) {
|
if (pop) {
|
||||||
// Usuwamy klasy animacji tymczasowo, by nie "wjeżdżał" co 5 sekund
|
// Temporarily remove animation classes to avoid reanimating every 5 seconds
|
||||||
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; // Restore scroll position
|
||||||
|
|
||||||
// Zwracamy animacje po ułamku sekundy
|
// Restore animations shortly after rendering
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
pop.classList.add('transition-all', 'duration-300');
|
pop.classList.add('transition-all', 'duration-300');
|
||||||
}, 50);
|
}, 50);
|
||||||
} else {
|
} else {
|
||||||
// Jeśli po filtracji dymku nie ma w DOM, czyścimy stan
|
// Clear state if filtering removed the popover from the DOM
|
||||||
state.openPopoverId = null;
|
state.openPopoverId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlobalExhibition(locations) {
|
function renderGlobalExhibition(allLocations, visibleLocations) {
|
||||||
const container = document.getElementById("nscExhibitionCard");
|
const container = document.getElementById("nscExhibitionCard");
|
||||||
const allT = state.terminals;
|
const allT = state.terminals;
|
||||||
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;
|
||||||
|
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 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";
|
||||||
@@ -452,7 +384,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white">Wszystkie Lokalizacje</h2>
|
<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ą (Galerie: ${locations.length})</p>
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Zarządzanie całą wystawą (${locationSummary})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -491,14 +423,14 @@
|
|||||||
|
|
||||||
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 z nowym przyciskiem -->
|
<!-- Card header with the details button -->
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div>
|
<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>
|
<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 class="text-xs text-slate-500 dark:text-slate-400 mt-1 font-mono">${loc.id}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Przycisk SZCZEGÓŁY (zamiast samego statustu 11/15) -->
|
<!-- Details button with status count -->
|
||||||
<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">
|
<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 class="w-2 h-2 rounded-full ${dotCls}"></span>
|
||||||
<span>${on} / ${total}</span>
|
<span>${on} / ${total}</span>
|
||||||
@@ -512,7 +444,7 @@
|
|||||||
<div class="h-full ${dotCls} transition-all duration-500" style="width: ${pct}%"></div>
|
<div class="h-full ${dotCls} transition-all duration-500" style="width: ${pct}%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sekcja KONTROLERY na kafelku -->
|
<!-- Controllers section on the card -->
|
||||||
<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="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="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>
|
<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>
|
||||||
@@ -528,7 +460,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sekcja OŚWIETLENIE na kafelku -->
|
<!-- Lighting section on the card -->
|
||||||
<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="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="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>
|
<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>
|
||||||
@@ -543,13 +475,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dymek (Popover) centralnie nad kafelkiem, wymiar w 100% dopasowany do kafelka w ramach siatki -->
|
<!-- Popover centered over the card and matched to the card size in the grid -->
|
||||||
<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 calutką ramkę) -->
|
<!-- Popover panel filling the card frame -->
|
||||||
<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 -->
|
<!-- Popover header -->
|
||||||
<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">
|
||||||
<i class="fa fa-list text-brand-500"></i> Szczegóły: ${escapeHtml(loc.label)}
|
<i class="fa fa-list text-brand-500"></i> Szczegóły: ${escapeHtml(loc.label)}
|
||||||
@@ -559,7 +491,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Obszar przewijany -->
|
<!-- Scrollable area -->
|
||||||
<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)}
|
||||||
${renderTerminalRows(lights, 'Oświetlenie', false)}
|
${renderTerminalRows(lights, 'Oświetlenie', false)}
|
||||||
@@ -578,7 +510,7 @@
|
|||||||
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
|
// Individual action buttons
|
||||||
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>
|
||||||
<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>
|
<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>
|
||||||
@@ -666,18 +598,18 @@
|
|||||||
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
|
// Preview mode skips FETCH requests
|
||||||
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) {
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
await new Promise(r => setTimeout(r, 100)); // Symulacja requestu
|
await new Promise(r => setTimeout(r, 100)); // Simulate request
|
||||||
const tRef = MOCK_DATA.find(m => m.id === t.id);
|
const tRef = MOCK_DATA.find(m => m.id === t.id);
|
||||||
if(tRef) tRef.status = command === COMMAND_SHUTDOWN ? 'Stopped' : 'Running';
|
if(tRef) tRef.status = command === COMMAND_SHUTDOWN ? 'Stopped' : 'Running';
|
||||||
} else {
|
} else {
|
||||||
// Produkcja
|
// Production request
|
||||||
try {
|
try {
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
body.set("command", command);
|
body.set("command", command);
|
||||||
@@ -704,5 +636,6 @@
|
|||||||
return String(str||"").replace(/[&<>"']/g, m => ({"&":"&","<":"<",">":">",'"':""","'":"'"})[m]);
|
return String(str||"").replace(/[&<>"']/g, m => ({"&":"&","<":"<",">":">",'"':""","'":"'"})[m]);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user