Skip to content

Instantly share code, notes, and snippets.

@bruvv
Last active June 18, 2026 10:19
Show Gist options
  • Select an option

  • Save bruvv/94c2df5c745a8be6c685854f8b56de22 to your computer and use it in GitHub Desktop.

Select an option

Save bruvv/94c2df5c745a8be6c685854f8b56de22 to your computer and use it in GitHub Desktop.
world-cup-2026-kiosk.user.js
// ==UserScript==
// @name Home Assistant World Cup 2026 Kiosk
// @namespace https://github.com/bruvv
// @version 1.5.1
// @description Wisselt automatisch tussen World Cup 2026-tabs, verbergt ongewenste onderdelen en herstelt bij navigatieproblemen.
// @author bruvv
// @match *://homeassistant.local:8123/world-cup-2026*
// @match *://homeassistan.local:8123/world-cup-2026*
// @updateURL https://gist.githubusercontent.com/bruvv/94c2df5c745a8be6c685854f8b56de22/raw/world-cup-2026-kiosk.user.js
// @downloadURL https://gist.githubusercontent.com/bruvv/94c2df5c745a8be6c685854f8b56de22/raw/world-cup-2026-kiosk.user.js
// @grant none
// @run-at document-start
// ==/UserScript==
(() => {
'use strict';
/*
* Aantal seconden dat iedere tab zichtbaar blijft.
*/
const SWITCH_INTERVAL_SECONDS = 30;
/*
* Aantal seconden wachten voordat de automatische rotatie begint.
*/
const STARTUP_DELAY_SECONDS = 10;
/*
* Tabs die automatisch worden getoond.
* Supporters wordt bewust overgeslagen.
*/
const TAB_PAGES = [
'live',
'overview',
'teams',
'fixtures',
'results',
'groups',
'knockout',
'players',
'records',
'stats',
'venues',
];
const CONFIG = {
switchIntervalMs: SWITCH_INTERVAL_SECONDS * 1000,
startupDelayMs: STARTUP_DELAY_SECONDS * 1000,
clickTimeoutMs: 8_000,
clickRetryDelayMs: 2_000,
pageHealthCheckIntervalMs: 10_000,
hideElementsIntervalMs: 1_000,
reloadDelayMs: 1_000,
requiredFailedPageChecks: 3,
};
let rotationTimer = null;
let pageHealthTimer = null;
let hideElementsTimer = null;
let navigationRunning = false;
let reloadRunning = false;
let failedPageChecks = 0;
function log(message, ...values) {
console.log(`[World Cup kiosk] ${message}`, ...values);
}
function logError(message, ...values) {
console.error(`[World Cup kiosk] ${message}`, ...values);
}
function sleep(milliseconds) {
return new Promise((resolve) => {
window.setTimeout(resolve, milliseconds);
});
}
function injectStyles() {
if (document.getElementById('world-cup-kiosk-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'world-cup-kiosk-styles';
style.textContent = `
.overview-premium-strip {
display: none !important;
visibility: hidden !important;
height: 0 !important;
min-height: 0 !important;
max-height: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
}
.wc-nav button[data-page="supporters"] {
display: none !important;
visibility: hidden !important;
}
`;
const target = document.head || document.documentElement;
if (target) {
target.appendChild(style);
}
}
function collectRoots(root = document, roots = []) {
if (!root || roots.includes(root)) {
return roots;
}
roots.push(root);
const elements = root.querySelectorAll?.('*') ?? [];
for (const element of elements) {
if (element.shadowRoot) {
collectRoots(element.shadowRoot, roots);
}
}
return roots;
}
function querySelectorDeep(selector) {
for (const root of collectRoots()) {
try {
const element = root.querySelector(selector);
if (element) {
return element;
}
} catch (error) {
logError(`Ongeldige selector: ${selector}`, error);
}
}
return null;
}
function querySelectorAllDeep(selector) {
const results = [];
const seen = new Set();
for (const root of collectRoots()) {
let elements;
try {
elements = root.querySelectorAll(selector);
} catch (error) {
continue;
}
for (const element of elements) {
if (!seen.has(element)) {
seen.add(element);
results.push(element);
}
}
}
return results;
}
function hideElement(element) {
if (!(element instanceof Element)) {
return;
}
element.style.setProperty('display', 'none', 'important');
element.style.setProperty('visibility', 'hidden', 'important');
element.style.setProperty('height', '0', 'important');
element.style.setProperty('min-height', '0', 'important');
element.style.setProperty('max-height', '0', 'important');
element.style.setProperty('margin', '0', 'important');
element.style.setProperty('padding', '0', 'important');
element.style.setProperty('overflow', 'hidden', 'important');
element.hidden = true;
element.setAttribute('aria-hidden', 'true');
}
function hideUnwantedElements() {
injectStyles();
const premiumStrips = querySelectorAllDeep(
'.overview-premium-strip',
);
for (const premiumStrip of premiumStrips) {
hideElement(premiumStrip);
}
const supportersButtons = querySelectorAllDeep(
'.wc-nav button[data-page="supporters"]',
);
for (const supportersButton of supportersButtons) {
hideElement(supportersButton);
}
}
function startHideElementsCheck() {
if (hideElementsTimer !== null) {
return;
}
hideUnwantedElements();
hideElementsTimer = window.setInterval(() => {
hideUnwantedElements();
}, CONFIG.hideElementsIntervalMs);
}
function isElementVisible(element) {
if (!(element instanceof Element)) {
return false;
}
const style = window.getComputedStyle(element);
const rectangle = element.getBoundingClientRect();
return (
!element.hidden &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
rectangle.width > 0 &&
rectangle.height > 0
);
}
function findTabButton(page) {
const buttons = querySelectorAllDeep(
`.wc-nav button[data-page="${page}"]`,
);
return (
buttons.find((button) => isElementVisible(button)) ??
buttons[0] ??
null
);
}
function getActivePage() {
const activeButton = querySelectorDeep(
'.wc-nav button.active[data-page]',
);
return activeButton?.dataset?.page ?? null;
}
function getAvailablePages() {
return querySelectorAllDeep(
'.wc-nav button[data-page]',
)
.map((button) => button.dataset.page)
.filter(Boolean);
}
function isNavigationAvailable() {
const availablePages = new Set(getAvailablePages());
return TAB_PAGES.every((page) => {
return availablePages.has(page);
});
}
function stopRotation() {
if (rotationTimer !== null) {
window.clearTimeout(rotationTimer);
rotationTimer = null;
log('Automatische rotatie gestopt');
}
}
function scheduleNextRotation() {
stopRotation();
if (reloadRunning) {
return;
}
rotationTimer = window.setTimeout(() => {
void rotateToNextTab();
}, CONFIG.switchIntervalMs);
}
function waitForActivePage(expectedPage) {
return new Promise((resolve) => {
const startedAt = Date.now();
const interval = window.setInterval(() => {
const activePage = getActivePage();
if (activePage === expectedPage) {
window.clearInterval(interval);
resolve(true);
return;
}
if (
Date.now() - startedAt >=
CONFIG.clickTimeoutMs
) {
window.clearInterval(interval);
resolve(false);
}
}, 100);
});
}
async function clickTab(page) {
let button = findTabButton(page);
if (!button) {
logError(`Tabknop niet gevonden: ${page}`);
return false;
}
if (getActivePage() === page) {
return true;
}
try {
button.scrollIntoView({
block: 'nearest',
inline: 'nearest',
});
button.click();
} catch (error) {
logError(`Klikken op ${page} gaf een fout`, error);
return false;
}
let succeeded = await waitForActivePage(page);
if (succeeded) {
log(`Tab geopend: ${page}`);
hideUnwantedElements();
return true;
}
logError(
`Eerste klik op ${page} werkte niet, opnieuw proberen`,
);
await sleep(CONFIG.clickRetryDelayMs);
button = findTabButton(page);
if (!button) {
return false;
}
try {
button.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
}),
);
} catch (error) {
logError(`Tweede klik op ${page} gaf een fout`, error);
return false;
}
succeeded = await waitForActivePage(page);
if (succeeded) {
log(`Tab na tweede poging geopend: ${page}`);
hideUnwantedElements();
}
return succeeded;
}
async function rotateToNextTab() {
if (navigationRunning || reloadRunning) {
return;
}
navigationRunning = true;
try {
hideUnwantedElements();
const activePage = getActivePage();
const activeIndex = TAB_PAGES.indexOf(activePage);
const nextIndex =
activeIndex >= 0
? (activeIndex + 1) % TAB_PAGES.length
: 0;
const nextPage = TAB_PAGES[nextIndex];
const succeeded = await clickTab(nextPage);
if (!succeeded) {
await reloadPage(
`tab ${nextPage} kon niet worden geopend`,
);
return;
}
scheduleNextRotation();
} finally {
navigationRunning = false;
}
}
async function reloadPage(reason) {
if (reloadRunning) {
return;
}
reloadRunning = true;
stopRotation();
logError(`Volledige pagina wordt herladen: ${reason}`);
await sleep(CONFIG.reloadDelayMs);
window.location.reload();
}
function runPageHealthCheck() {
hideUnwantedElements();
if (reloadRunning) {
return;
}
if (isNavigationAvailable()) {
failedPageChecks = 0;
if (
rotationTimer === null &&
!navigationRunning
) {
scheduleNextRotation();
}
return;
}
failedPageChecks += 1;
logError(
`Navigatiecontrole mislukt: ${failedPageChecks}/${CONFIG.requiredFailedPageChecks}`,
);
if (
failedPageChecks >=
CONFIG.requiredFailedPageChecks
) {
void reloadPage(
'de World Cup-navigatie is niet beschikbaar',
);
}
}
function startPageHealthCheck() {
if (pageHealthTimer !== null) {
return;
}
pageHealthTimer = window.setInterval(() => {
runPageHealthCheck();
}, CONFIG.pageHealthCheckIntervalMs);
}
window.addEventListener('pageshow', () => {
failedPageChecks = 0;
reloadRunning = false;
hideUnwantedElements();
});
injectStyles();
startHideElementsCheck();
startPageHealthCheck();
window.setTimeout(() => {
hideUnwantedElements();
if (!isNavigationAvailable()) {
void reloadPage(
'de World Cup-navigatie werd niet geladen',
);
return;
}
log(
`Automatische rotatie gestart met ${SWITCH_INTERVAL_SECONDS} seconden per tab`,
);
scheduleNextRotation();
}, CONFIG.startupDelayMs);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment