web - refactor core modules, harden authorization, simplify endpoints
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../general_files/ini/runtime_env.php';
|
||||
define('HA_BASE_URL', rtrim(intranet_env('HA_BASE_URL'), '/'));
|
||||
define('HA_TOKEN', intranet_env('HA_TOKEN'));
|
||||
@@ -0,0 +1,236 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const CONTENT_REFRESH_MS = 30_000;
|
||||
const INDOOR_REFRESH_MS = 10_000;
|
||||
const OUTDOOR_REFRESH_MS = 30_000;
|
||||
const VERTICAL_SPEED_PX_PER_SECOND = 7;
|
||||
const HORIZONTAL_SPEED_PX_PER_SECOND = 10;
|
||||
|
||||
const WEATHER_ICONS = {
|
||||
cloudy: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><defs><filter id="weather-blur" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur in="SourceAlpha" stdDeviation="2.5"/><feOffset dx="0" dy="2" result="off"/><feComponentTransfer><feFuncA type="linear" slope="0.15"/></feComponentTransfer><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g filter="url(#weather-blur)"><path d="M22 46h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 20 29a9 9 0 0 0 2 17z" fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><path d="M18 46h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 17 31a8 8 0 0 0 1 15z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/></g></svg>`,
|
||||
sun: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"><circle cx="32" cy="32" r="10" fill="#FFD34D" stroke="#fff"/><path d="M32 6v8M32 50v8M6 32h8M50 32h8M13.5 13.5l5.7 5.7M44.8 44.8l5.7 5.7M50.5 13.5l-5.7 5.7M19.2 44.8l-5.7 5.7"/></g></svg>`,
|
||||
moon: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><path d="M41 8a22 22 0 1 0 15 35A18 18 0 0 1 41 8z" fill="#DDE7FF" stroke="#fff" stroke-width="2" stroke-linejoin="round"/></svg>`,
|
||||
cloudy_day: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g><g transform="translate(4,0)"><circle cx="22" cy="22" r="9" fill="#FFD34D" stroke="#fff" stroke-width="2"/><g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"><path d="M22 6v6M22 32v6M6 22h6M32 22h6M10.5 10.5l4.2 4.2M29.3 29.3l4.2 4.2M33.5 10.5l-4.2 4.2M14.7 29.3l-4.2 4.2"/></g></g><g transform="translate(8,10)"><path d="M18 36h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 16 19a9 9 0 0 0 2 17z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><path d="M22 36h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 21 21a8 8 0 0 0 1 15z" fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/></g></g></svg>`,
|
||||
cloudy_night: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g><path d="M38 10a16 16 0 1 0 11 26A13 13 0 0 1 38 10z" fill="#DDE7FF" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><g transform="translate(10,18)"><path d="M14 28h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 12 11a9 9 0 0 0 2 17z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><path d="M18 28h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 17 13a8 8 0 0 0 1 15z" fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/></g></g></svg>`,
|
||||
rain_shower: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g><path d="M20 36h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 19a9 9 0 0 0 2 17z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><g stroke="#fff" stroke-width="2" stroke-linecap="round"><path d="M24 42l-3 6"/><path d="M34 42l-3 6"/><path d="M44 42l-3 6"/></g></g></svg>`,
|
||||
rain: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g><path d="M20 34h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 17a9 9 0 0 0 2 17z" fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><g stroke="#fff" stroke-width="2" stroke-linecap="round"><path d="M22 40l-3 8"/><path d="M32 40l-3 8"/><path d="M42 40l-3 8"/><path d="M52 40l-3 8"/></g></g></svg>`,
|
||||
thunder: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g><path d="M18 34h28a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 16 17a9 9 0 0 0 2 17z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><path d="M32 36l-6 12h7l-3 10 12-16h-7l3-6z" fill="#FFD34D" stroke="#fff" stroke-width="2" stroke-linejoin="round"/></g></svg>`,
|
||||
snowy: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g><path d="M20 34h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 17a9 9 0 0 0 2 17z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><g fill="#fff"><circle cx="24" cy="44" r="2"/><circle cx="34" cy="44" r="2"/><circle cx="44" cy="44" r="2"/><circle cx="29" cy="52" r="2"/><circle cx="39" cy="52" r="2"/></g></g></svg>`,
|
||||
mist: `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true"><g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.95"><path d="M14 24h36"/><path d="M10 32h44"/><path d="M14 40h36"/><path d="M18 48h28"/></g></svg>`,
|
||||
};
|
||||
|
||||
function setText(selector, value) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.textContent = value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
const clock = document.getElementById('uhrzeit');
|
||||
if (clock) {
|
||||
clock.textContent = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAnnouncements(announcements) {
|
||||
const container = document.querySelector('.news-scroll');
|
||||
if (!container || !Array.isArray(announcements)) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.replaceChildren();
|
||||
announcements.forEach((announcement) => {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'news-box';
|
||||
|
||||
const date = document.createElement('div');
|
||||
date.className = 'news-date';
|
||||
date.append(document.createTextNode(announcement.date_label || ''));
|
||||
if (announcement.end_label) {
|
||||
date.append(document.createElement('br'), document.createTextNode(announcement.end_label));
|
||||
}
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.append(document.createTextNode(announcement.title || ''));
|
||||
|
||||
if (announcement.subtitle) {
|
||||
const subtitle = document.createElement('div');
|
||||
subtitle.className = 'news-subtitle';
|
||||
subtitle.textContent = announcement.subtitle;
|
||||
body.append(subtitle);
|
||||
}
|
||||
|
||||
if (announcement.meta) {
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'news-subtitle';
|
||||
const italic = document.createElement('i');
|
||||
italic.textContent = announcement.meta;
|
||||
meta.append(italic);
|
||||
body.append(meta);
|
||||
}
|
||||
|
||||
box.append(date, body);
|
||||
container.append(box);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPosters(posters) {
|
||||
const container = document.querySelector('.aushang-flex');
|
||||
if (!container || !Array.isArray(posters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.replaceChildren();
|
||||
if (posters.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.style.textAlign = 'left';
|
||||
empty.textContent = 'Keine Aushänge vorgeplant.';
|
||||
container.append(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
posters.forEach((poster) => {
|
||||
const image = document.createElement('img');
|
||||
image.src = poster.url;
|
||||
image.alt = poster.alt || 'Aushang';
|
||||
image.style.height = '95%';
|
||||
image.addEventListener('load', () => scheduleScrollingSetup(0), { once: true });
|
||||
container.append(image);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMonthImages(images) {
|
||||
if (!Array.isArray(images)) {
|
||||
return;
|
||||
}
|
||||
|
||||
images.forEach((image) => {
|
||||
const element = document.querySelector(`[data-month-page="${image.page}"]`);
|
||||
if (element && image.url && element.getAttribute('src') !== image.url) {
|
||||
element.src = image.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderIndoor(indoor) {
|
||||
if (indoor) {
|
||||
setText('#temperature_txt', `${indoor.value}${indoor.unit || '°C'}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOutdoor(outdoor) {
|
||||
const weather = document.getElementById('wetter');
|
||||
if (!weather || !outdoor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const icon = WEATHER_ICONS[outdoor.icon] || WEATHER_ICONS.cloudy;
|
||||
weather.replaceChildren();
|
||||
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.innerHTML = icon;
|
||||
const temperature = document.createElement('div');
|
||||
temperature.className = 'text-outdoor';
|
||||
temperature.textContent = `${outdoor.temperature}°C`;
|
||||
weather.append(iconContainer, temperature);
|
||||
}
|
||||
|
||||
function renderContent(data) {
|
||||
setText('[data-dashboard-date]', data.date_label);
|
||||
if (data.service) {
|
||||
setText('.bible', data.service.bibelwort);
|
||||
setText('.verse', data.service.bibelvers);
|
||||
setText('.GB', data.service.eingang_nummer);
|
||||
setText('.GB-title', data.service.eingang_titel);
|
||||
}
|
||||
|
||||
renderAnnouncements(data.announcements);
|
||||
renderPosters(data.posters);
|
||||
renderMonthImages(data.month_images);
|
||||
scheduleScrollingSetup();
|
||||
}
|
||||
|
||||
function startPolling(resource, interval, render) {
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const response = await fetch(`data.php?resource=${encodeURIComponent(resource)}`, { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
render(data);
|
||||
} catch (error) {
|
||||
console.error(`Dashboard-Aktualisierung (${resource}) fehlgeschlagen:`, error);
|
||||
} finally {
|
||||
window.setTimeout(refresh, interval);
|
||||
}
|
||||
};
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
function applyAnimation(element, distance, speed, animationName) {
|
||||
element.style.animation = 'none';
|
||||
if (distance <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = animationName === 'scroll-h' ? '--scroll-distance-h' : '--scroll-distance-v';
|
||||
element.style.setProperty(variable, `-${distance}px`);
|
||||
element.style.animationName = animationName;
|
||||
element.style.animationDuration = `${Math.max(1, distance / speed)}s`;
|
||||
element.style.animationTimingFunction = 'linear';
|
||||
element.style.animationIterationCount = 'infinite';
|
||||
}
|
||||
|
||||
function setupScrolling() {
|
||||
const posterContainer = document.querySelector('.aushang-flex');
|
||||
if (posterContainer) {
|
||||
const images = Array.from(posterContainer.querySelectorAll('img'));
|
||||
const totalWidth = images.reduce((sum, image) => {
|
||||
const styles = window.getComputedStyle(image);
|
||||
return sum + image.offsetWidth + (parseFloat(styles.marginRight) || 0);
|
||||
}, 0);
|
||||
applyAnimation(
|
||||
posterContainer,
|
||||
Math.max(0, totalWidth - posterContainer.offsetWidth),
|
||||
HORIZONTAL_SPEED_PX_PER_SECOND,
|
||||
'scroll-h'
|
||||
);
|
||||
}
|
||||
|
||||
const newsContainer = document.querySelector('.news-container');
|
||||
const news = document.querySelector('.news-scroll');
|
||||
if (newsContainer && news) {
|
||||
applyAnimation(
|
||||
news,
|
||||
Math.max(0, news.scrollHeight - newsContainer.offsetHeight),
|
||||
VERTICAL_SPEED_PX_PER_SECOND,
|
||||
'scroll-v'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let scrollingTimer = null;
|
||||
function scheduleScrollingSetup(delay = 80) {
|
||||
if (scrollingTimer !== null) {
|
||||
window.clearTimeout(scrollingTimer);
|
||||
}
|
||||
scrollingTimer = window.setTimeout(setupScrolling, delay);
|
||||
}
|
||||
|
||||
updateClock();
|
||||
window.setInterval(updateClock, 1000);
|
||||
window.addEventListener('load', () => scheduleScrollingSetup(50));
|
||||
window.addEventListener('resize', () => scheduleScrollingSetup(100));
|
||||
startPolling('content', CONTENT_REFRESH_MS, renderContent);
|
||||
startPolling('indoor', INDOOR_REFRESH_MS, (data) => renderIndoor(data.indoor));
|
||||
startPolling('outdoor', OUTDOOR_REFRESH_MS, (data) => renderOutdoor(data.outdoor));
|
||||
})();
|
||||
@@ -1,58 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
require_once __DIR__ . '/../contentpanel/insertion/news_cleanup.php';
|
||||
require_once __DIR__ . '/util/dashboard_helpers.php';
|
||||
|
||||
// DB-Verbindung zentral laden (liefert PDO-Objekt zurück)
|
||||
$pdo = require __DIR__ . '/../../general_files/ini/database.php';
|
||||
$openWeatherApiKey = intranet_env('OPENWEATHER_API_KEY');
|
||||
|
||||
$heute = date('Y-m-d');
|
||||
|
||||
// --- Gottesdienst (Bibelwort & Eingangslied) ---
|
||||
$sql = "SELECT bibelwort, bibelvers, eingang_nummer, eingang_titel
|
||||
FROM gottesdienst
|
||||
WHERE bibeldatum = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$heute]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row) {
|
||||
$bibelwort = $row['bibelwort'];
|
||||
$bibelvers = $row['bibelvers'];
|
||||
$eingangNummer = $row['eingang_nummer'];
|
||||
$eingangTitel = $row['eingang_titel'];
|
||||
} else {
|
||||
$bibelwort = "Bibelwort nicht eingetragen";
|
||||
$bibelvers = "Bibelvers nicht gefunden.";
|
||||
$eingangNummer = "N/A";
|
||||
$eingangTitel = "Eingangslied nicht gefunden";
|
||||
}
|
||||
|
||||
// --- Ansagen ---
|
||||
$sqlAnsagen = "SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
|
||||
FROM ansagen
|
||||
WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
|
||||
ORDER BY ansage_datum_von ASC";
|
||||
$stmtAnsagen = $pdo->prepare($sqlAnsagen);
|
||||
$stmtAnsagen->execute([$heute, $heute]);
|
||||
$ansagenResult = $stmtAnsagen->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Verbindung schließen (optional, aber sauber)
|
||||
$content = dashboard_fetch_content($pdo, dashboard_now());
|
||||
$pdo = null;
|
||||
|
||||
// --- Aushänge ---
|
||||
$pngDirectoryUrl = '../contentpanel/insertion/news/png/';
|
||||
|
||||
// Abgelaufene Aushänge einschließlich Original-PDF bereinigen
|
||||
cleanupExpiredNews();
|
||||
|
||||
$pngDirectoryFs = realpath(
|
||||
__DIR__ . '/../contentpanel/insertion/news/png'
|
||||
);
|
||||
|
||||
$pngFiles = ($pngDirectoryFs && is_dir($pngDirectoryFs))
|
||||
? scandir($pngDirectoryFs)
|
||||
: [];
|
||||
$service = $content['service'];
|
||||
$announcements = $content['announcements'];
|
||||
$posters = $content['posters'];
|
||||
$monthImages = $content['month_images'];
|
||||
$stylesheetVersion = (int) filemtime(__DIR__ . '/stylo.css');
|
||||
$scriptVersion = (int) filemtime(__DIR__ . '/dashboard.js');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
@@ -62,10 +24,9 @@ $pngFiles = ($pngDirectoryFs && is_dir($pngDirectoryFs))
|
||||
<title>NAK Reinickendorf</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/intranet_repo/projects/_central/img/nak-logo.svg">
|
||||
<link rel="shortcut icon" href="/intranet_repo/projects/_central/img/favicon.ico">
|
||||
<link rel="stylesheet" href="stylo.css?v=<?php echo time(); ?>">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="stylo.css?v=<?= $stylesheetVersion ?>">
|
||||
<link href="https://fonts.googleapis.com/css2?family=SUSE:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Anton&display=swap" rel="stylesheet">
|
||||
<script src="dashboard.js?v=<?= $scriptVersion ?>" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content-board">
|
||||
@@ -73,63 +34,29 @@ $pngFiles = ($pngDirectoryFs && is_dir($pngDirectoryFs))
|
||||
<div class="content-column-right-upper-box">
|
||||
<div class="content-column-right-upper-box-right">
|
||||
<div class="content-item-time">
|
||||
<div class="time">
|
||||
<div id="uhrzeit"></div>
|
||||
</div>
|
||||
<div class="date">
|
||||
<center><?php
|
||||
$months = ["UNDEFINED", "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
|
||||
echo date("d. ") . $months[intval(date("m"))];
|
||||
?></center>
|
||||
</div>
|
||||
<div class="time"><div id="uhrzeit"></div></div>
|
||||
<div class="date" style="text-align: center" data-dashboard-date><?= dashboard_html($content['date_label']) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="content-item-daily">
|
||||
<div class="header-sticky-note">
|
||||
<div>Heutige Ansagen</div>
|
||||
</div>
|
||||
<div class="header-sticky-note"><div>Heutige Ansagen</div></div>
|
||||
<div class="news-container">
|
||||
<div class="news-scroll">
|
||||
<?php foreach ($ansagenResult as $ansage) : ?>
|
||||
<?php foreach ($announcements as $announcement): ?>
|
||||
<div class="news-box">
|
||||
<div class="news-date">
|
||||
<?php
|
||||
$wochentage = [
|
||||
'Mon' => 'Mo','Tue' => 'Di','Wed' => 'Mi','Thu' => 'Do',
|
||||
'Fri' => 'Fr','Sat' => 'Sa','Sun' => 'So'
|
||||
];
|
||||
$datum_von_timestamp = strtotime($ansage['ansage_datum_von']);
|
||||
$heute = date("Y-m-d");
|
||||
$morgen = date("Y-m-d", strtotime('+1 day'));
|
||||
$datum_von_format = date("Y-m-d", $datum_von_timestamp);
|
||||
|
||||
if ($datum_von_format === $heute) {
|
||||
echo "Heute";
|
||||
} elseif ($datum_von_format === $morgen) {
|
||||
echo "Morgen";
|
||||
} else {
|
||||
$datum_von = date("d.m.", $datum_von_timestamp);
|
||||
$wochentag_englisch = date("D", $datum_von_timestamp);
|
||||
$wochentag = $wochentage[$wochentag_englisch] ?? $wochentag_englisch;
|
||||
echo $wochentag . ' ' . $datum_von;
|
||||
}
|
||||
if (!empty($ansage['ansage_datum_bis'])) {
|
||||
$datum_bis = date("d.m.", strtotime($ansage['ansage_datum_bis']));
|
||||
echo "<br> bis " . $datum_bis;
|
||||
}
|
||||
?>
|
||||
<?= dashboard_html($announcement['date_label']) ?>
|
||||
<?php if ($announcement['end_label'] !== ''): ?>
|
||||
<br><?= dashboard_html($announcement['end_label']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<?php echo htmlspecialchars($ansage['ansage_titel']); ?>
|
||||
<?php if (!empty($ansage['ansage_untertitel'])) : ?>
|
||||
<div class="news-subtitle"><?php echo htmlspecialchars($ansage['ansage_untertitel']); ?></div>
|
||||
<?= dashboard_html($announcement['title']) ?>
|
||||
<?php if ($announcement['subtitle'] !== ''): ?>
|
||||
<div class="news-subtitle"><?= dashboard_html($announcement['subtitle']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($ansage['ansage_uhrzeit']) && !empty($ansage['ansage_ort'])) : ?>
|
||||
<div class="news-subtitle"><i><?php echo date("H:i", strtotime($ansage['ansage_uhrzeit'])) . ' Uhr'; ?>, <?php echo htmlspecialchars($ansage['ansage_ort']); ?></i></div>
|
||||
<?php elseif (!empty($ansage['ansage_uhrzeit'])) : ?>
|
||||
<div class="news-subtitle"><i><?php echo date("H:i", strtotime($ansage['ansage_uhrzeit'])) . ' Uhr'; ?></i></div>
|
||||
<?php elseif (!empty($ansage['ansage_ort'])) : ?>
|
||||
<div class="news-subtitle"><i><?php echo htmlspecialchars($ansage['ansage_ort']); ?></i></div>
|
||||
<?php if ($announcement['meta'] !== ''): ?>
|
||||
<div class="news-subtitle"><i><?= dashboard_html($announcement['meta']) ?></i></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,32 +68,24 @@ $pngFiles = ($pngDirectoryFs && is_dir($pngDirectoryFs))
|
||||
|
||||
<div class="content-column-right-upper-box-left">
|
||||
<div class="content-item-middle-top">
|
||||
<div class="header-sticky-note">
|
||||
<div>Bibelwort</div>
|
||||
</div>
|
||||
<div class="bible"><?php echo $bibelwort; ?></div>
|
||||
<div class="verse"><?php echo $bibelvers; ?></div>
|
||||
<div class="header-sticky-note"><div>Bibelwort</div></div>
|
||||
<div class="bible"><?= dashboard_html($service['bibelwort']) ?></div>
|
||||
<div class="verse"><?= dashboard_html($service['bibelvers']) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="content-column-double-item">
|
||||
<div class="content-item">
|
||||
<div class="header-sticky-note">
|
||||
<div>Eingangslied</div>
|
||||
</div>
|
||||
<div class="GB"><?php echo $eingangNummer; ?></div>
|
||||
<div class="GB-title"><?php echo $eingangTitel; ?></div>
|
||||
<div class="header-sticky-note"><div>Eingangslied</div></div>
|
||||
<div class="GB"><?= dashboard_html($service['eingang_nummer']) ?></div>
|
||||
<div class="GB-title"><?= dashboard_html($service['eingang_titel']) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="content-item">
|
||||
<div class="header-sticky-note">
|
||||
<div>Temperatur</div>
|
||||
</div>
|
||||
<div class="header-sticky-note"><div>Temperatur</div></div>
|
||||
<div class="temperature">
|
||||
<div class="container"><div id="wetter"></div></div>
|
||||
<div class="container">
|
||||
<div id="wetter"></div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 60 60" width="64px" height="64px" baseProfile="basic">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 60 60" width="64px" height="64px" baseProfile="basic" aria-hidden="true">
|
||||
<polygon fill="#66e1ff" points="46,22.11 46,24.42 43,26.15 43,34.24 25,44.63 14,38.28 14,35.97 12,34.81 10,35.97 7,34.24 7,20.38 4,20.96 4,18.65 22,8.26 34,5.95"/>
|
||||
<polygon fill="#4292a6" points="25,30.774 28,34.815 43,26.155 43,34.238 25,44.63"/>
|
||||
<polygon fill="#ff5757" points="16,18.649 4,20.959 4,18.649 16,16.34 28,32.506 28,34.815"/>
|
||||
@@ -180,333 +99,42 @@ $pngFiles = ($pngDirectoryFs && is_dir($pngDirectoryFs))
|
||||
<polygon fill="#52b1cc" points="17,34.238 22,37.124 22,34.815 19,33.083"/>
|
||||
<polygon fill="#ff5757" points="12,34.815 14,35.97 14,29.041 12,27.887"/>
|
||||
</svg>
|
||||
<div class="text-indoor" id='temperature_txt'></div>
|
||||
<div class="text-indoor" id="temperature_txt"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /left upper box -->
|
||||
</div> <!-- /right upper -->
|
||||
|
||||
<div class="content-item-bottom-box">
|
||||
<div class="header-sticky-note">
|
||||
<div>Aushänge</div>
|
||||
</div>
|
||||
<div class="aushang-flex">
|
||||
<?php
|
||||
$hasPng = false;
|
||||
foreach ($pngFiles as $file) {
|
||||
if (pathinfo($file, PATHINFO_EXTENSION) === 'png') {
|
||||
$hasPng = true;
|
||||
echo '<img src="' . $pngDirectoryUrl . $file . '" alt="Aushang" height="95%">';
|
||||
}
|
||||
}
|
||||
if (!$hasPng) {
|
||||
echo '<p style="text-align: left;">Keine Aushänge vorgeplant.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /right column -->
|
||||
|
||||
<div class="content-item-bottom-box">
|
||||
<div class="header-sticky-note"><div>Aushänge</div></div>
|
||||
<div class="aushang-flex">
|
||||
<?php if ($posters === []): ?>
|
||||
<p style="text-align: left">Keine Aushänge vorgeplant.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($posters as $poster): ?>
|
||||
<img src="<?= dashboard_html($poster['url']) ?>" alt="<?= dashboard_html($poster['alt']) ?>" height="95%">
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-column-left">
|
||||
<div class="content-item-video">
|
||||
<!-- leerer Container, kein Video/HLS -->
|
||||
<div class="video-container" aria-label="Leer"></div>
|
||||
</div>
|
||||
|
||||
<div class="content-item-month">
|
||||
<div class="header-sticky-note">
|
||||
<div>Monatsplan</div>
|
||||
</div>
|
||||
<div class="header-sticky-note"><div>Monatsplan</div></div>
|
||||
<div class="content-item-month-cell">
|
||||
<img src="month/terminuebersicht-1.png?v=<?php echo time(); ?>" alt="image">
|
||||
<img src="month/terminuebersicht-2.png?v=<?php echo time(); ?>" alt="image">
|
||||
<?php foreach ($monthImages as $image): ?>
|
||||
<img src="<?= dashboard_html($image['url']) ?>" data-month-page="<?= (int) $image['page'] ?>" alt="Monatsplan Seite <?= (int) $image['page'] ?>">
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /left column -->
|
||||
</div> <!-- /content-board -->
|
||||
|
||||
<script>
|
||||
function updateClock() {
|
||||
var now = new Date();
|
||||
var uhrzeitElement = document.getElementById("uhrzeit");
|
||||
var stunden = now.getHours();
|
||||
var minuten = now.getMinutes();
|
||||
uhrzeitElement.innerHTML = + padZero(stunden) + ':' + padZero(minuten);
|
||||
}
|
||||
function padZero(number) { return number < 10 ? '0' + number : number; }
|
||||
updateClock();
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
// OpenWeather (Außentemperatur+Icon)
|
||||
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=52.571291&lon=13.310422&appid=<?= rawurlencode($openWeatherApiKey) ?>&units=metric&language=german`;
|
||||
// Wetter-Icons
|
||||
const cloudy = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<defs>
|
||||
<filter id="blur" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="2.5"/>
|
||||
<feOffset dx="0" dy="2" result="off"/>
|
||||
<feComponentTransfer><feFuncA type="linear" slope="0.15"/></feComponentTransfer>
|
||||
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter="url(#blur)">
|
||||
<path d="M22 46h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 20 29a9 9 0 0 0 2 17z" fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M18 46h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 17 31a8 8 0 0 0 1 15z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const sun = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="32" cy="32" r="10" fill="#FFD34D" stroke="#fff"/>
|
||||
<path d="M32 6v8M32 50v8M6 32h8M50 32h8M13.5 13.5l5.7 5.7M44.8 44.8l5.7 5.7M50.5 13.5l-5.7 5.7M19.2 44.8l-5.7 5.7"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const moon = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<path d="M41 8a22 22 0 1 0 15 35A18 18 0 0 1 41 8z" fill="#DDE7FF" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
|
||||
const cloudy_day = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g>
|
||||
<g transform="translate(4,0)">
|
||||
<circle cx="22" cy="22" r="9" fill="#FFD34D" stroke="#fff" stroke-width="2"/>
|
||||
<g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M22 6v6M22 32v6M6 22h6M32 22h6M10.5 10.5l4.2 4.2M29.3 29.3l4.2 4.2M33.5 10.5l-4.2 4.2M14.7 29.3l-4.2 4.2"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(8,10)">
|
||||
<path d="M18 36h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 16 19a9 9 0 0 0 2 17z"
|
||||
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M22 36h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 21 21a8 8 0 0 0 1 15z"
|
||||
fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const cloudy_night = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g>
|
||||
<path d="M38 10a16 16 0 1 0 11 26A13 13 0 0 1 38 10z" fill="#DDE7FF" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<g transform="translate(10,18)">
|
||||
<path d="M14 28h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 12 11a9 9 0 0 0 2 17z"
|
||||
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M18 28h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 17 13a8 8 0 0 0 1 15z"
|
||||
fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const rain_shower = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g>
|
||||
<path d="M20 36h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 19a9 9 0 0 0 2 17z"
|
||||
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<g stroke="#fff" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M24 42l-3 6" />
|
||||
<path d="M34 42l-3 6" />
|
||||
<path d="M44 42l-3 6" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const rain = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g>
|
||||
<path d="M20 34h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 17a9 9 0 0 0 2 17z"
|
||||
fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<g stroke="#fff" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M22 40l-3 8" />
|
||||
<path d="M32 40l-3 8" />
|
||||
<path d="M42 40l-3 8" />
|
||||
<path d="M52 40l-3 8" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const thunder = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g>
|
||||
<path d="M18 34h28a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 16 17a9 9 0 0 0 2 17z"
|
||||
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M32 36l-6 12h7l-3 10 12-16h-7l3-6z"
|
||||
fill="#FFD34D" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const snowy = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g>
|
||||
<path d="M20 34h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 17a9 9 0 0 0 2 17z"
|
||||
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
|
||||
<g fill="#fff">
|
||||
<circle cx="24" cy="44" r="2"/>
|
||||
<circle cx="34" cy="44" r="2"/>
|
||||
<circle cx="44" cy="44" r="2"/>
|
||||
<circle cx="29" cy="52" r="2"/>
|
||||
<circle cx="39" cy="52" r="2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const mist = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
|
||||
<g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.95">
|
||||
<path d="M14 24h36"/>
|
||||
<path d="M10 32h44"/>
|
||||
<path d="M14 40h36"/>
|
||||
<path d="M18 48h28"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
|
||||
async function getWeather() {
|
||||
try {
|
||||
var response = await fetch(apiUrl);
|
||||
var data = await response.json();
|
||||
if (data.cod === 200) {
|
||||
var weatherIcon = data.weather[0].icon;
|
||||
var temperature = data.main.temp;
|
||||
var weatherElement = document.getElementById('wetter');
|
||||
if (["04d","04n","03d","03n"].includes(weatherIcon)) {
|
||||
weatherElement.innerHTML = '<div>' + cloudy + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (weatherIcon === "02d") {
|
||||
weatherElement.innerHTML = '<div>' + cloudy_day + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (weatherIcon === "02n") {
|
||||
weatherElement.innerHTML = '<div>' + cloudy_night + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (weatherIcon === "01d") {
|
||||
weatherElement.innerHTML = '<div>' + sun + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (weatherIcon === "01n") {
|
||||
weatherElement.innerHTML = '<div>' + moon + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (["09d","09n","10n"].includes(weatherIcon)) {
|
||||
weatherElement.innerHTML = '<div>' + rain_shower + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (weatherIcon === "10d") {
|
||||
weatherElement.innerHTML = '<div>' + rain + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (["11d","11n"].includes(weatherIcon)) {
|
||||
weatherElement.innerHTML = '<div>' + thunder + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (["13d","13n"].includes(weatherIcon)) {
|
||||
weatherElement.innerHTML = '<div>' + snowy + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
} else if (["50d","50n"].includes(weatherIcon)) {
|
||||
weatherElement.innerHTML = '<div>' + mist + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
|
||||
}
|
||||
} else {
|
||||
console.error('Fehler beim Abrufen des Wetters:', data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Wetters:', error.message);
|
||||
}
|
||||
}
|
||||
getWeather();
|
||||
setInterval(getWeather, 30000);
|
||||
|
||||
// INNENTEMPERATUR: jetzt über Home Assistant (Server-Proxy)
|
||||
function updateTemperature() {
|
||||
const entityId = 'sensor.temp_temperatur';
|
||||
fetch('ha_temperature.php?entity_id=' + encodeURIComponent(entityId))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error('HA Fehler:', data);
|
||||
return;
|
||||
}
|
||||
document.getElementById('temperature_txt').innerText = `${data.value}${data.unit || '°C'}`;
|
||||
})
|
||||
.catch(err => console.error('Fetch Fehler:', err));
|
||||
}
|
||||
updateTemperature();
|
||||
setInterval(updateTemperature, 10000);
|
||||
|
||||
function updateContent() {
|
||||
fetch('update_content.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error('Fehler:', data.error);
|
||||
return;
|
||||
}
|
||||
document.querySelector('.bible').innerText = data.bibelwort;
|
||||
document.querySelector('.verse').innerText = data.bibelvers;
|
||||
document.querySelector('.GB').innerText = data.eingang_nummer;
|
||||
document.querySelector('.GB-title').innerText = data.eingang_titel;
|
||||
setupScrolling();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Abrufen der Daten:', error);
|
||||
});
|
||||
}
|
||||
updateContent();
|
||||
setInterval(updateContent, 30000);
|
||||
|
||||
function setupScrolling() {
|
||||
const SPEED_V = 7; // News vertikal
|
||||
const SPEED_H = 10; // Aushänge horizontal
|
||||
|
||||
const containerFlex = document.querySelector('.aushang-flex');
|
||||
if (containerFlex) {
|
||||
const images = containerFlex.querySelectorAll('img');
|
||||
const measureAndApplyH = () => {
|
||||
let totalWidth = 0;
|
||||
images.forEach(img => {
|
||||
const styles = window.getComputedStyle(img);
|
||||
const mr = parseFloat(styles.marginRight) || 0;
|
||||
totalWidth += (img.offsetWidth || 0) + mr;
|
||||
});
|
||||
const containerWidth = containerFlex.offsetWidth || 0;
|
||||
const scrollDistanceH = Math.max(0, totalWidth - containerWidth);
|
||||
containerFlex.style.setProperty('--scroll-distance-h', `-${scrollDistanceH}px`);
|
||||
const durationH = Math.max(1, scrollDistanceH / SPEED_H);
|
||||
if (scrollDistanceH === 0) {
|
||||
containerFlex.style.animation = 'none';
|
||||
} else {
|
||||
containerFlex.style.animationName = 'scroll-h';
|
||||
containerFlex.style.animationDuration = `${durationH}s`;
|
||||
containerFlex.style.animationTimingFunction = 'linear';
|
||||
containerFlex.style.animationIterationCount = 'infinite';
|
||||
}
|
||||
};
|
||||
let pending = 0;
|
||||
images.forEach(img => {
|
||||
if (!img.complete || img.naturalWidth === 0) {
|
||||
pending++;
|
||||
img.addEventListener('load', () => {
|
||||
pending--;
|
||||
if (pending === 0) measureAndApplyH();
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
if (pending === 0) measureAndApplyH();
|
||||
}
|
||||
|
||||
const containerNews = document.querySelector('.news-container');
|
||||
const scrollBox = document.querySelector('.news-scroll');
|
||||
if (containerNews && scrollBox) {
|
||||
const totalHeight = scrollBox.scrollHeight || 0;
|
||||
const visibleHeight = containerNews.offsetHeight || 0;
|
||||
const scrollDistanceV = Math.max(0, totalHeight - visibleHeight);
|
||||
scrollBox.style.setProperty('--scroll-distance-v', `-${scrollDistanceV}px`);
|
||||
const durationV = Math.max(1, scrollDistanceV / SPEED_V);
|
||||
if (scrollDistanceV === 0) {
|
||||
scrollBox.style.animation = 'none';
|
||||
} else {
|
||||
scrollBox.style.animationName = 'scroll-v';
|
||||
scrollBox.style.animationDuration = `${durationV}s`;
|
||||
scrollBox.style.animationTimingFunction = 'linear';
|
||||
scrollBox.style.animationIterationCount = 'infinite';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const news = document.querySelector('.news-scroll');
|
||||
const aushang = document.querySelector('.aushang-flex');
|
||||
if (news) news.style.animation = 'none';
|
||||
if (aushang) aushang.style.animation = 'none';
|
||||
setTimeout(setupScrolling, 50);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
const news = document.querySelector('.news-scroll');
|
||||
const aushang = document.querySelector('.aushang-flex');
|
||||
if (news) news.style.animation = 'none';
|
||||
if (aushang) aushang.style.animation = 'none';
|
||||
setTimeout(setupScrolling, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
require_once __DIR__ . '/util/dashboard_helpers.php';
|
||||
|
||||
$resource = (string) ($_GET['resource'] ?? 'content');
|
||||
$pdo = null;
|
||||
|
||||
try {
|
||||
switch ($resource) {
|
||||
case 'content':
|
||||
$pdo = require __DIR__ . '/../../general_files/ini/database.php';
|
||||
$payload = dashboard_fetch_content($pdo, dashboard_now());
|
||||
break;
|
||||
case 'indoor':
|
||||
$payload = ['indoor' => dashboard_fetch_indoor_temperature()];
|
||||
break;
|
||||
case 'outdoor':
|
||||
$payload = ['outdoor' => dashboard_fetch_outdoor_weather()];
|
||||
break;
|
||||
default:
|
||||
http_response_code(400);
|
||||
$payload = ['error' => 'Unbekannte Dashboard-Ressource.'];
|
||||
}
|
||||
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $exception) {
|
||||
http_response_code(502);
|
||||
error_log('Dashboard data endpoint (' . $resource . '): ' . $exception->getMessage());
|
||||
echo json_encode(['error' => 'Dashboard-Daten konnten nicht geladen werden.'], JSON_UNESCAPED_UNICODE);
|
||||
} finally {
|
||||
$pdo = null;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Token/URL aus externer Config laden
|
||||
require_once __DIR__ . '/config.local.php';
|
||||
|
||||
// Entity-ID (Standard: dein Sensor aus der Liste)
|
||||
$entityId = isset($_GET['entity_id']) ? $_GET['entity_id'] : 'sensor.temp_temperatur';
|
||||
|
||||
$url = rtrim(HA_BASE_URL, '/') . '/api/states/' . urlencode($entityId);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . HA_TOKEN,
|
||||
'Content-Type: application/json'
|
||||
],
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false || $httpCode < 200 || $httpCode >= 300) {
|
||||
http_response_code(502);
|
||||
echo json_encode([
|
||||
'error' => 'Home Assistant request failed',
|
||||
'details' => $error ?: ('HTTP ' . $httpCode)
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!is_array($data) || !isset($data['state'])) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Unexpected HA response']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$val = $data['state'];
|
||||
$unit = $data['attributes']['unit_of_measurement'] ?? '°C';
|
||||
|
||||
$CALIBRATION_OFFSET = +0;
|
||||
|
||||
if (is_numeric($val)) {
|
||||
$number = round((float)$val + $CALIBRATION_OFFSET, 1);
|
||||
} else {
|
||||
$number = $val;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'entity_id' => $entityId,
|
||||
'value' => $number,
|
||||
'unit' => $unit,
|
||||
'friendly_name' => $data['attributes']['friendly_name'] ?? null,
|
||||
'last_changed' => $data['last_changed'] ?? null,
|
||||
]);
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
// DB-Verbindung zentral laden
|
||||
$pdo = require __DIR__ . '/../../general_files/ini/database.php';
|
||||
|
||||
$heute = date('Y-m-d');
|
||||
|
||||
try {
|
||||
// --- Bibelwort, Bibelvers und Eingangslied ---
|
||||
$sql = "SELECT bibelwort, bibelvers, eingang_nummer, eingang_titel
|
||||
FROM gottesdienst
|
||||
WHERE bibeldatum = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$heute]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row) {
|
||||
$bibelwort = $row['bibelwort'];
|
||||
$bibelvers = $row['bibelvers'];
|
||||
$eingangNummer = $row['eingang_nummer'];
|
||||
$eingangTitel = $row['eingang_titel'];
|
||||
} else {
|
||||
$bibelwort = "Bibelwort nicht eingetragen";
|
||||
$bibelvers = "Bibelvers nicht gefunden.";
|
||||
$eingangNummer = "N/A";
|
||||
$eingangTitel = "Eingangslied nicht gefunden";
|
||||
}
|
||||
|
||||
// --- Ansagen ---
|
||||
$sqlAnsagen = "SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
|
||||
FROM ansagen
|
||||
WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
|
||||
ORDER BY ansage_datum_von ASC";
|
||||
$stmtAnsagen = $pdo->prepare($sqlAnsagen);
|
||||
$stmtAnsagen->execute([$heute, $heute]);
|
||||
$ansagenResult = $stmtAnsagen->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Rückgabe als JSON
|
||||
echo json_encode([
|
||||
'bibelwort' => $bibelwort,
|
||||
'bibelvers' => $bibelvers,
|
||||
'eingang_nummer' => $eingangNummer,
|
||||
'eingang_titel' => $eingangTitel,
|
||||
'ansagen' => $ansagenResult
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
// Optional: error_log($e->getMessage());
|
||||
echo json_encode(['error' => 'Fehler bei der Verbindung zur Datenbank'], JSON_UNESCAPED_UNICODE);
|
||||
} finally {
|
||||
$pdo = null;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../contentpanel/insertion/util/contentpanel_helpers.php';
|
||||
require_once __DIR__ . '/../../../general_files/ini/runtime_env.php';
|
||||
|
||||
const DASHBOARD_TIMEZONE = 'Europe/Berlin';
|
||||
const DASHBOARD_HA_ENTITY_ID = 'sensor.temp_temperatur';
|
||||
const DASHBOARD_OPENWEATHER_LATITUDE = '52.571291';
|
||||
const DASHBOARD_OPENWEATHER_LONGITUDE = '13.310422';
|
||||
const DASHBOARD_NEWS_URL = '/intranet_repo/projects/contentpanel/insertion/news/png/';
|
||||
|
||||
function dashboard_now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('now', new DateTimeZone(DASHBOARD_TIMEZONE));
|
||||
}
|
||||
|
||||
function dashboard_html($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function dashboard_fetch_content(PDO $pdo, DateTimeImmutable $now): array
|
||||
{
|
||||
$today = $now->format('Y-m-d');
|
||||
|
||||
$serviceStatement = $pdo->prepare(
|
||||
'SELECT bibelwort, bibelvers, eingang_nummer, eingang_titel
|
||||
FROM gottesdienst
|
||||
WHERE bibeldatum = ?'
|
||||
);
|
||||
$serviceStatement->execute([$today]);
|
||||
$service = $serviceStatement->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
$announcementStatement = $pdo->prepare(
|
||||
'SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
|
||||
FROM ansagen
|
||||
WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
|
||||
ORDER BY ansage_datum_von ASC'
|
||||
);
|
||||
$announcementStatement->execute([$today, $today]);
|
||||
$announcements = $announcementStatement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($announcements as &$announcement) {
|
||||
$announcement = dashboard_prepare_announcement($announcement, $now);
|
||||
}
|
||||
unset($announcement);
|
||||
|
||||
contentpanel_cleanup_expired_news();
|
||||
|
||||
return [
|
||||
'date_label' => dashboard_date_label($now),
|
||||
'service' => [
|
||||
'bibelwort' => (string) ($service['bibelwort'] ?? 'Bibelwort nicht eingetragen'),
|
||||
'bibelvers' => (string) ($service['bibelvers'] ?? 'Bibelvers nicht gefunden.'),
|
||||
'eingang_nummer' => (string) ($service['eingang_nummer'] ?? 'N/A'),
|
||||
'eingang_titel' => (string) ($service['eingang_titel'] ?? 'Eingangslied nicht gefunden'),
|
||||
],
|
||||
'announcements' => $announcements,
|
||||
'posters' => dashboard_list_posters(),
|
||||
'month_images' => dashboard_month_images(),
|
||||
];
|
||||
}
|
||||
|
||||
function dashboard_date_label(DateTimeImmutable $date): string
|
||||
{
|
||||
static $months = [
|
||||
1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||
];
|
||||
|
||||
return sprintf('%02d. %s', (int) $date->format('d'), $months[(int) $date->format('n')]);
|
||||
}
|
||||
|
||||
function dashboard_prepare_announcement(array $announcement, DateTimeImmutable $now): array
|
||||
{
|
||||
$startRaw = substr((string) ($announcement['ansage_datum_von'] ?? ''), 0, 10);
|
||||
$endRaw = substr((string) ($announcement['ansage_datum_bis'] ?? ''), 0, 10);
|
||||
$timeRaw = substr((string) ($announcement['ansage_uhrzeit'] ?? ''), 0, 5);
|
||||
|
||||
$startDate = DateTimeImmutable::createFromFormat('!Y-m-d', $startRaw, $now->getTimezone());
|
||||
$tomorrow = $now->setTime(0, 0)->modify('+1 day');
|
||||
|
||||
if ($startDate && $startDate->format('Y-m-d') === $now->format('Y-m-d')) {
|
||||
$dateLabel = 'Heute';
|
||||
} elseif ($startDate && $startDate->format('Y-m-d') === $tomorrow->format('Y-m-d')) {
|
||||
$dateLabel = 'Morgen';
|
||||
} elseif ($startDate) {
|
||||
static $weekdays = [1 => 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
$dateLabel = $weekdays[(int) $startDate->format('N')] . ' ' . $startDate->format('d.m.');
|
||||
} else {
|
||||
$dateLabel = $startRaw;
|
||||
}
|
||||
|
||||
$endLabel = '';
|
||||
if ($endRaw !== '') {
|
||||
$endDate = DateTimeImmutable::createFromFormat('!Y-m-d', $endRaw, $now->getTimezone());
|
||||
$endLabel = $endDate ? 'bis ' . $endDate->format('d.m.') : 'bis ' . $endRaw;
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
if ($timeRaw !== '') {
|
||||
$meta[] = $timeRaw . ' Uhr';
|
||||
}
|
||||
$place = trim((string) ($announcement['ansage_ort'] ?? ''));
|
||||
if ($place !== '') {
|
||||
$meta[] = $place;
|
||||
}
|
||||
|
||||
return [
|
||||
'date_label' => $dateLabel,
|
||||
'end_label' => $endLabel,
|
||||
'title' => (string) ($announcement['ansage_titel'] ?? ''),
|
||||
'subtitle' => (string) ($announcement['ansage_untertitel'] ?? ''),
|
||||
'meta' => implode(', ', $meta),
|
||||
];
|
||||
}
|
||||
|
||||
function dashboard_list_posters(): array
|
||||
{
|
||||
$files = glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*.png') ?: [];
|
||||
natsort($files);
|
||||
|
||||
$posters = [];
|
||||
foreach ($files as $file) {
|
||||
if (!is_file($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$basename = basename($file);
|
||||
$posters[] = [
|
||||
'url' => DASHBOARD_NEWS_URL . rawurlencode($basename) . '?v=' . (int) filemtime($file),
|
||||
'alt' => 'Aushang',
|
||||
];
|
||||
}
|
||||
|
||||
return array_values($posters);
|
||||
}
|
||||
|
||||
function dashboard_month_images(): array
|
||||
{
|
||||
$images = [];
|
||||
foreach ([1, 2] as $page) {
|
||||
$filename = 'terminuebersicht-' . $page . '.png';
|
||||
$path = __DIR__ . '/../month/' . $filename;
|
||||
$version = is_file($path) ? (int) filemtime($path) : 0;
|
||||
$images[] = [
|
||||
'page' => $page,
|
||||
'url' => 'month/' . $filename . '?v=' . $version,
|
||||
];
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
function dashboard_fetch_indoor_temperature(): array
|
||||
{
|
||||
$baseUrl = rtrim(intranet_env('HA_BASE_URL'), '/');
|
||||
$token = intranet_env('HA_TOKEN');
|
||||
$url = $baseUrl . '/api/states/' . rawurlencode(DASHBOARD_HA_ENTITY_ID);
|
||||
$data = dashboard_http_json($url, ['Authorization: Bearer ' . $token]);
|
||||
|
||||
if (!array_key_exists('state', $data)) {
|
||||
throw new RuntimeException('Unerwartete Home-Assistant-Antwort.');
|
||||
}
|
||||
|
||||
$value = $data['state'];
|
||||
if (is_numeric($value)) {
|
||||
$value = round((float) $value, 1);
|
||||
}
|
||||
|
||||
return [
|
||||
'value' => $value,
|
||||
'unit' => (string) ($data['attributes']['unit_of_measurement'] ?? '°C'),
|
||||
];
|
||||
}
|
||||
|
||||
function dashboard_fetch_outdoor_weather(): array
|
||||
{
|
||||
$query = http_build_query([
|
||||
'lat' => DASHBOARD_OPENWEATHER_LATITUDE,
|
||||
'lon' => DASHBOARD_OPENWEATHER_LONGITUDE,
|
||||
'appid' => intranet_env('OPENWEATHER_API_KEY'),
|
||||
'units' => 'metric',
|
||||
'lang' => 'de',
|
||||
], '', '&', PHP_QUERY_RFC3986);
|
||||
|
||||
$data = dashboard_http_json('https://api.openweathermap.org/data/2.5/weather?' . $query);
|
||||
$temperature = $data['main']['temp'] ?? null;
|
||||
$icon = (string) ($data['weather'][0]['icon'] ?? '');
|
||||
|
||||
if (!is_numeric($temperature)) {
|
||||
throw new RuntimeException('Unerwartete OpenWeather-Antwort.');
|
||||
}
|
||||
|
||||
return [
|
||||
'temperature' => (int) round((float) $temperature),
|
||||
'icon' => dashboard_weather_kind($icon),
|
||||
];
|
||||
}
|
||||
|
||||
function dashboard_weather_kind(string $icon): string
|
||||
{
|
||||
return match ($icon) {
|
||||
'04d', '04n', '03d', '03n' => 'cloudy',
|
||||
'02d' => 'cloudy_day',
|
||||
'02n' => 'cloudy_night',
|
||||
'01d' => 'sun',
|
||||
'01n' => 'moon',
|
||||
'09d', '09n', '10n' => 'rain_shower',
|
||||
'10d' => 'rain',
|
||||
'11d', '11n' => 'thunder',
|
||||
'13d', '13n' => 'snowy',
|
||||
'50d', '50n' => 'mist',
|
||||
default => 'cloudy',
|
||||
};
|
||||
}
|
||||
|
||||
function dashboard_http_json(string $url, array $headers = []): array
|
||||
{
|
||||
$handle = curl_init($url);
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('HTTP-Anfrage konnte nicht initialisiert werden.');
|
||||
}
|
||||
|
||||
curl_setopt_array($handle, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => array_merge(['Accept: application/json'], $headers),
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 3,
|
||||
]);
|
||||
|
||||
try {
|
||||
$response = curl_exec($handle);
|
||||
$status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($handle);
|
||||
} finally {
|
||||
curl_close($handle);
|
||||
}
|
||||
|
||||
if ($response === false || $status < 200 || $status >= 300) {
|
||||
throw new RuntimeException($error !== '' ? $error : 'HTTP ' . $status);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $exception) {
|
||||
throw new RuntimeException('Ungültige JSON-Antwort.', 0, $exception);
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new RuntimeException('Unerwartete JSON-Antwort.');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
Reference in New Issue
Block a user