From 17b159c563278c3b3b8b3884be37f8d5025ce0c7 Mon Sep 17 00:00:00 2001
From: zwlucas <lucas.fariamo08@gmail.com>
Date: Fri, 29 May 2026 17:53:13 -0300
Subject: feat: implement kiosk mode with wake lock and UI adjustments

Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
---
 frontend/src/routes/+page.svelte | 82 ++++++++++++++++++++++++++++++++++++++--
 1 file changed, 78 insertions(+), 4 deletions(-)

(limited to 'frontend/src/routes/+page.svelte')

diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte
index 172e6b8..6a57fd4 100644
--- a/frontend/src/routes/+page.svelte
+++ b/frontend/src/routes/+page.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-	import { onMount } from 'svelte';
+	import { onMount, tick } from 'svelte';
 	import { deleteService } from '$lib/api';
 	import ServiceCard from '$lib/components/ServiceCard.svelte';
 	import AddServiceModal from '$lib/components/AddServiceModal.svelte';
@@ -18,6 +18,12 @@
 	let error = $state('');
 	let showModal = $state(false);
 
+	let isKiosk = $state(false);
+	let kioskFloating = $state(false);
+	let kioskTimer: ReturnType<typeof setTimeout> | undefined;
+	let wakeLock: WakeLockSentinel | null = $state(null);
+	let pulseId = $state(0);
+
 	$effect(() => {
 		if (!initialized && data) {
 			services = [...data.services];
@@ -78,20 +84,60 @@
 		}
 	}
 
+	function toggleKiosk() {
+		isKiosk = !isKiosk;
+		requestAnimationFrame(() => {
+			if (isKiosk) {
+				document.documentElement.classList.add('kiosk-mode');
+				requestWakeLock();
+			} else {
+				document.documentElement.classList.remove('kiosk-mode');
+				releaseWakeLock();
+				kioskFloating = false;
+			}
+		});
+	}
+
+	async function requestWakeLock() {
+		try {
+			if ('wakeLock' in navigator) {
+				wakeLock = await navigator.wakeLock.request('screen');
+				wakeLock.addEventListener('release', () => { wakeLock = null; });
+			}
+		} catch { /* wake lock not available */ }
+	}
+
+	function releaseWakeLock() {
+		if (wakeLock) {
+			wakeLock.release();
+			wakeLock = null;
+		}
+	}
+
+	function handleKioskMouseMove() {
+		if (!isKiosk) return;
+		kioskFloating = true;
+		clearTimeout(kioskTimer);
+		kioskTimer = setTimeout(() => { kioskFloating = false; }, 3000);
+	}
+
 	onMount(() => {
 		const unsub = subscribeHeartbeats((hb) => {
 			let wasUp: boolean | undefined;
+			let changed = false;
 			services = services.map((s) => {
 				if (s.id !== hb.service_id) return s;
 				if (s.last_heartbeat) {
 					wasUp = s.last_heartbeat.is_up;
 				}
+				changed = true;
 				return {
 					...s,
 					last_heartbeat: hb,
 					heartbeats: [...s.heartbeats, hb].slice(-30)
 				};
 			});
+			if (changed) pulseId += 1;
 
 			const svc = services.find((s) => s.id === hb.service_id);
 			if (!svc) return;
@@ -114,11 +160,38 @@
 	<title>YAUM — Yet Another Uptime Monitor</title>
 </svelte:head>
 
-<div class="mb-8">
-	<h1 class="text-2xl font-semibold tracking-tight text-white">Dashboard</h1>
-	<p class="mt-1 text-sm text-[var(--text-muted)]">Status dos serviços monitorados</p>
+<div class="mb-8 flex items-start justify-between" class:kiosk-hidden={isKiosk}>
+	<div>
+		<h1 class="text-2xl font-semibold tracking-tight text-white">Dashboard</h1>
+		<p class="mt-1 text-sm text-[var(--text-muted)]">Status dos serviços monitorados</p>
+	</div>
+	<button
+		onclick={toggleKiosk}
+		class="kiosk-hidden shrink-0 rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2 text-xs font-medium text-[var(--text-muted)] transition-all hover:border-[var(--green)]/30 hover:text-[var(--green)]"
+		title="Modo TV — tela cheia para monitores de parede"
+	>
+		<svg class="-ml-0.5 mr-1.5 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
+			<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h13.5A2.25 2.25 0 0121 5.25z" />
+		</svg>
+		Modo TV
+	</button>
 </div>
 
+<!-- Floating exit button (kiosk only) -->
+{#if isKiosk}
+	<button
+		onclick={toggleKiosk}
+		class="fixed bottom-6 right-6 z-50 rounded-xl border border-white/10 bg-black/60 px-4 py-2.5 text-xs font-medium text-white/70 backdrop-blur-sm transition-all duration-300 hover:bg-black/80 hover:text-white"
+		class:opacity-0={!kioskFloating}
+		class:opacity-100={kioskFloating}
+		class:pointer-events-none={!kioskFloating}
+	>
+		Sair do Modo TV
+	</button>
+{/if}
+
+<!-- eslint-disable-next-line svelte/no-reactive-reassign -->
+<div onmousemove={handleKioskMouseMove} role="region" class:kiosk-active={isKiosk} class:pulse={pulseId > 0} data-pulse={pulseId}>
 {#if error}
 	<div class="rounded-2xl border border-[var(--red)]/20 bg-[var(--red)]/5 p-8 text-center">
 		<svg class="mx-auto mb-3 h-8 w-8 text-[var(--red)]/60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
@@ -164,6 +237,7 @@
 		<ActivityLog entries={logEntries} />
 	</div>
 {/if}
+</div>
 
 <AddServiceModal
 	show={showModal}
-- 
cgit v1.2.3

