|
|
|
|
@ -1,8 +1,9 @@
|
|
|
|
|
<script src="assets/vendor/tailwindcss-3.4.17-cdn.js"></script>
|
|
|
|
|
<script src="assets/vendor/tailwindcss-3.4.17-cdn.js"></script>
|
|
|
|
|
<link rel="stylesheet" href="assets/vendor/fontawesome/css/all.min.css">
|
|
|
|
|
<script>
|
|
|
|
|
tailwind.config = {
|
|
|
|
|
darkMode: 'class',
|
|
|
|
|
important: '#nscDashboardRoot',
|
|
|
|
|
theme: {
|
|
|
|
|
extend: {
|
|
|
|
|
colors: {
|
|
|
|
|
@ -23,54 +24,175 @@
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
<style>
|
|
|
|
|
/* Wider, modern macOS-style scrollbar for popovers */
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
|
|
|
#nscDashboardRoot {
|
|
|
|
|
all: initial;
|
|
|
|
|
display: block;
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
text-align: initial;
|
|
|
|
|
-webkit-font-smoothing: antialiased;
|
|
|
|
|
-moz-osx-font-smoothing: grayscale;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot .nsc-dashboard-bg {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: 0;
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
background-color: #f8fafc;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
.dark #nscDashboardRoot .nsc-dashboard-bg {
|
|
|
|
|
background-color: #0f172a;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot .nsc-dashboard-content {
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: 1920px;
|
|
|
|
|
margin-inline: auto;
|
|
|
|
|
}
|
|
|
|
|
.dark #nscDashboardRoot {
|
|
|
|
|
color: #e2e8f0;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot,
|
|
|
|
|
#nscDashboardRoot *,
|
|
|
|
|
#nscDashboardRoot *::before,
|
|
|
|
|
#nscDashboardRoot *::after {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button,
|
|
|
|
|
#nscDashboardRoot input {
|
|
|
|
|
font: inherit;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot :where(h1, h2, h3, h4, h5, p, button, input) {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot :where(h1, h2, h3, h4, h5, p) {
|
|
|
|
|
padding: 0;
|
|
|
|
|
border: 0;
|
|
|
|
|
background: transparent;
|
|
|
|
|
font-style: normal;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot :where(button, input) {
|
|
|
|
|
color: inherit;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button {
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot input {
|
|
|
|
|
box-sizing: border-box !important;
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button {
|
|
|
|
|
box-sizing: border-box !important;
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
font-size: inherit !important;
|
|
|
|
|
line-height: inherit !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button {
|
|
|
|
|
min-width: 0 !important;
|
|
|
|
|
min-height: 0 !important;
|
|
|
|
|
border-width: 0;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.text-xs {
|
|
|
|
|
font-size: 0.75rem !important;
|
|
|
|
|
line-height: 1rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.text-sm {
|
|
|
|
|
font-size: 0.875rem !important;
|
|
|
|
|
line-height: 1.25rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.px-2 {
|
|
|
|
|
padding-left: 0.5rem !important;
|
|
|
|
|
padding-right: 0.5rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.px-3 {
|
|
|
|
|
padding-left: 0.75rem !important;
|
|
|
|
|
padding-right: 0.75rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.px-4 {
|
|
|
|
|
padding-left: 1rem !important;
|
|
|
|
|
padding-right: 1rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.px-5 {
|
|
|
|
|
padding-left: 1.25rem !important;
|
|
|
|
|
padding-right: 1.25rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.py-1 {
|
|
|
|
|
padding-top: 0.25rem !important;
|
|
|
|
|
padding-bottom: 0.25rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.py-1\.5 {
|
|
|
|
|
padding-top: 0.375rem !important;
|
|
|
|
|
padding-bottom: 0.375rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.py-2 {
|
|
|
|
|
padding-top: 0.5rem !important;
|
|
|
|
|
padding-bottom: 0.5rem !important;
|
|
|
|
|
}
|
|
|
|
|
#nscDashboardRoot button.py-2\.5 {
|
|
|
|
|
padding-top: 0.625rem !important;
|
|
|
|
|
padding-bottom: 0.625rem !important;
|
|
|
|
|
}
|
|
|
|
|
/* Wider, modern macOS-style scrollbar for popovers */
|
|
|
|
|
#nscDashboardRoot .custom-scrollbar::-webkit-scrollbar {
|
|
|
|
|
width: 12px;
|
|
|
|
|
}
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
|
|
|
#nscDashboardRoot .custom-scrollbar::-webkit-scrollbar-track {
|
|
|
|
|
background: transparent;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
|
|
|
#nscDashboardRoot .custom-scrollbar::-webkit-scrollbar-thumb {
|
|
|
|
|
background-color: rgba(156, 163, 175, 0.5); /* Semi-transparent gray */
|
|
|
|
|
border: 3px solid transparent; /* Inner border */
|
|
|
|
|
background-clip: padding-box;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
|
|
|
|
.dark #nscDashboardRoot .custom-scrollbar::-webkit-scrollbar-thumb {
|
|
|
|
|
background-color: rgba(107, 114, 128, 0.6);
|
|
|
|
|
}
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
#nscDashboardRoot .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
<div id="nscDashboardRoot">
|
|
|
|
|
<div class="nsc-dashboard-bg" aria-hidden="true"></div>
|
|
|
|
|
<div class="nsc-dashboard-content">
|
|
|
|
|
<div id="nscTerminalDashboard" class="flex flex-col gap-6">
|
|
|
|
|
<!-- The dashboard UI is injected here by JavaScript -->
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Custom confirm modal replacing 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">
|
|
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
|
|
|
<div class="w-10 h-10 rounded-full bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 flex items-center justify-center text-xl flex-shrink-0">
|
|
|
|
|
<i class="fa fa-question-circle"></i>
|
|
|
|
|
<!-- Custom confirm modal replacing 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">
|
|
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
|
|
|
<div class="w-10 h-10 rounded-full bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 flex items-center justify-center text-xl flex-shrink-0">
|
|
|
|
|
<i class="fa fa-question-circle"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 class="text-xl font-bold text-slate-900 dark:text-white" id="customConfirmTitle">Potwierdzenie</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-sm text-slate-600 dark:text-slate-300 mb-6 pl-13 font-medium leading-relaxed" id="customConfirmMessage">
|
|
|
|
|
Czy na pewno...
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex justify-end gap-3 pt-4 border-t border-slate-100 dark:border-slate-700">
|
|
|
|
|
<button id="customConfirmCancel" class="px-5 py-2.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-600 font-semibold transition-colors">
|
|
|
|
|
Anuluj
|
|
|
|
|
</button>
|
|
|
|
|
<button id="customConfirmOk" class="px-5 py-2.5 bg-brand-600 hover:bg-brand-700 text-white rounded-xl font-semibold transition-colors shadow-md shadow-brand-500/20">
|
|
|
|
|
Potwierdź Akcję
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 class="text-xl font-bold text-slate-900 dark:text-white" id="customConfirmTitle">Potwierdzenie</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-sm text-slate-600 dark:text-slate-300 mb-6 pl-13 font-medium leading-relaxed" id="customConfirmMessage">
|
|
|
|
|
Czy na pewno...
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex justify-end gap-3 pt-4 border-t border-slate-100 dark:border-slate-700">
|
|
|
|
|
<button id="customConfirmCancel" class="px-5 py-2.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-600 font-semibold transition-colors">
|
|
|
|
|
Anuluj
|
|
|
|
|
</button>
|
|
|
|
|
<button id="customConfirmOk" class="px-5 py-2.5 bg-brand-600 hover:bg-brand-700 text-white rounded-xl font-semibold transition-colors shadow-md shadow-brand-500/20">
|
|
|
|
|
Potwierdź Akcję
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -273,6 +395,16 @@
|
|
|
|
|
<i class="fa fa-info-circle"></i> Inicjalizacja...
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="nscCommandProgress" class="hidden bg-white dark:bg-slate-800 rounded-xl p-4 shadow-soft border border-slate-200 dark:border-slate-700">
|
|
|
|
|
<div class="flex items-center justify-between gap-4 text-xs font-bold mb-2">
|
|
|
|
|
<span id="nscCommandProgressLabel" class="text-slate-600 dark:text-slate-300">Wysyłanie poleceń...</span>
|
|
|
|
|
<span id="nscCommandProgressCount" class="text-slate-900 dark:text-white">0 / 0</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="h-2.5 w-full bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
|
|
|
<div id="nscCommandProgressBar" class="h-full bg-brand-500 transition-all duration-300" style="width: 0%"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Global summary view -->
|
|
|
|
|
<div id="nscExhibitionCard"></div>
|
|
|
|
|
|
|
|
|
|
@ -288,6 +420,26 @@
|
|
|
|
|
el.innerHTML = `<i class="fa ${isError ? 'fa-exclamation-triangle' : 'fa-check-circle'}"></i> ${msg}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setCommandProgress(sent, total, label) {
|
|
|
|
|
const progress = document.getElementById("nscCommandProgress");
|
|
|
|
|
const progressBar = document.getElementById("nscCommandProgressBar");
|
|
|
|
|
const progressCount = document.getElementById("nscCommandProgressCount");
|
|
|
|
|
const progressLabel = document.getElementById("nscCommandProgressLabel");
|
|
|
|
|
if (!progress || !progressBar || !progressCount || !progressLabel) return;
|
|
|
|
|
|
|
|
|
|
const pct = total ? Math.round((sent / total) * 100) : 0;
|
|
|
|
|
progress.classList.remove("hidden");
|
|
|
|
|
progressLabel.textContent = label;
|
|
|
|
|
progressCount.textContent = `${sent} / ${total}`;
|
|
|
|
|
progressBar.style.width = `${pct}%`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideCommandProgress(delayMs = 1200) {
|
|
|
|
|
const progress = document.getElementById("nscCommandProgress");
|
|
|
|
|
if (!progress) return;
|
|
|
|
|
setTimeout(() => progress.classList.add("hidden"), delayMs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openPopover(locId) {
|
|
|
|
|
document.querySelectorAll('[id^="popover-"]').forEach(p => closePopover(p.id.replace('popover-', '')));
|
|
|
|
|
state.openPopoverId = locId;
|
|
|
|
|
@ -416,7 +568,7 @@
|
|
|
|
|
const on = loc.terminals.filter(isTerminalOn).length;
|
|
|
|
|
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 === 'UVP');
|
|
|
|
|
const onPcs = pcs.filter(isTerminalOn).length;
|
|
|
|
|
|
|
|
|
|
const lights = loc.terminals.filter(t => t.player === 'CTRL');
|
|
|
|
|
@ -591,7 +743,7 @@
|
|
|
|
|
async function executeCommand(command, locationId, type) {
|
|
|
|
|
const targets = state.terminals.filter(t => {
|
|
|
|
|
if(locationId !== 'all' && t.location_id !== locationId) return false;
|
|
|
|
|
if(type !== 'all' && (type === 'computer' ? t.player !== 'PC' : t.player !== 'CTRL')) return false;
|
|
|
|
|
if(type !== 'all' && (type === 'computer' ? t.player !== 'UVP' : t.player !== 'CTRL')) return false;
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
if(!targets.length) return setStatus("Brak urządzeń spełniających kryteria.", true);
|
|
|
|
|
@ -601,12 +753,14 @@
|
|
|
|
|
async function executeCommandBulk(command, targets, scopeLabel) {
|
|
|
|
|
state.busy = true;
|
|
|
|
|
setStatus(`Wysyłanie polecenia ${command} do ${targets.length} urządzeń (${scopeLabel})...`, false);
|
|
|
|
|
setCommandProgress(0, targets.length, `Wysłano polecenie do 0 z ${targets.length} urządzeń`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Preview mode skips FETCH requests
|
|
|
|
|
const isPreview = window.location.protocol === 'about:' || window.location.protocol === 'blob:' || window.location.hostname === '';
|
|
|
|
|
|
|
|
|
|
let failedCount = 0;
|
|
|
|
|
let sentCount = 0;
|
|
|
|
|
|
|
|
|
|
for(const t of targets) {
|
|
|
|
|
if (isPreview) {
|
|
|
|
|
@ -618,18 +772,21 @@
|
|
|
|
|
try {
|
|
|
|
|
const body = new URLSearchParams();
|
|
|
|
|
body.set("command", command);
|
|
|
|
|
body.set("tag_id", t.id);
|
|
|
|
|
const url = `${API_COMMAND}&terminal_id=${encodeURIComponent(t.id)}`;
|
|
|
|
|
|
|
|
|
|
const res = await fetch(API_COMMAND, { method: "POST", body: body });
|
|
|
|
|
const res = await fetch(url, { method: "POST", body: body });
|
|
|
|
|
if(!res.ok) failedCount++;
|
|
|
|
|
} catch(e) {
|
|
|
|
|
failedCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sentCount++;
|
|
|
|
|
setCommandProgress(sentCount, targets.length, `Wysłano polecenie do ${sentCount} z ${targets.length} urządzeń`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
hideCommandProgress();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => refreshTerminals(true), 1500);
|
|
|
|
|
} finally {
|
|
|
|
|
|