After using the basic version of my Raspberry Pi setup as a digital picture frame for a few weeks in everyday life, it was clear that the system was stable – but the dashboard itself could be significantly improved.
I have described the basis here:
Raspberry Pi as a digital picture frame in kiosk mode – hardware, setup & troubleshooting
This update builds exactly on this and describes the final, corrected version, which is now running permanently and unattended.
Aim of the dashboard update
The new dashboard should:
- make several Allsky outputs directly usable on the display
- Be touch-enabled
- Play videos stably
- contain no special logic or workarounds
- have defined states in the event of an error
In short: less clever, more robust.
Dashboard structure (final)
The interface consists of three areas:
- Current image (large, clickable → full screen)
- “Last night” keogram with link to time-lapse video in full screen
- Keogram “last day” with link to time-lapse video in full screen
- Startrail without link
Dashboard: index.html
The dashboard itself is a simple HTML file with CSS grid and periodic reload of the images.
<img id="latestImage" src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT" onclick="window.location.href='fullscreen.html'">
The keograms link directly to the timelapse view:
<div class="thumb" onclick="window.location.href='timelapse.html?night=1'"> <img src="https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=1"> </div>
The images are updated regularly:
setInterval(() => { latestImage.src = 'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now(); }, 25000);index.html – Dashboard (complete, final)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=1280, height=800, initial-scale=1.0, user-scalable=no">
<title> </title>
<style>
html, body {
margin: 0;
padding: 0;
width: 1280px;
height: 800px;
background: #000;
color: #fff;
font-family: Arial, Helvetica, sans-serif;
overflow: hidden;
}
.page {
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.8s ease;
}
.page.visible {
opacity: 1;
}
.grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
height: 100%;
padding: 20px;
box-sizing: border-box;
}
h1 {
font-size: 28px;
margin: 0 0 12px 0;
font-weight: normal;
}
h2 {
font-size: 22px;
margin: 0 0 10px 0;
font-weight: normal;
}
.main img {
width: 100%;
height: 100%;
object-fit: contain;
cursor: pointer;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 30px;
}
.thumb img {
width: 100%;
border-radius: 4px;
border: 1px solid #333;
}
.thumb p {
margin: 6px 0 0 0;
font-size: 16px;
color: #ccc;
}
</style>
</head>
<body>
<div class="page" id="page">
<div class="grid">
<div class="main">
<h1>Current image</h1>
<img id="latestImage"
src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT"
onclick="window.location.href='fullscreen.html'">
</div>
<div class="sidebar">
<div>
<h2>Time lapse</h2>
<div class="thumb"
onclick="window.location.href='timelapse.html?night=1'">
<img id="keogramNight"
src="https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=1&ts=INIT">
<p>Last night</p>
</div>
<div class="thumb"
onclick="window.location.href='timelapse.html?night=0'">
<img id="keogramDay"
src="https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=0&ts=INIT">
<p>Last day</p>
</div>
</div>
<div>
<h2>Start trail</h2>
<div class="thumb">
<img id="startrail"
src="https://access.allsky-rodgau.de/indi-allsky/lateststartrail?ts=INIT">
<p>Last startrail</p>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('page').classList.add('visible');
});
setInterval(() => {
latestImage.src =
'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now();
}, 25000);
setInterval(() => {
keogramNight.src =
'https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=1&ts=' + Date.now();
keogramDay.src =
'https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=0&ts=' + Date.now();
startrail.src =
'https://access.allsky-rodgau.de/indi-allsky/lateststartrail?ts=' + Date.now();
}, 3600000);
</script>
</body>
</html>
Pull timelapse videos via redirect views
A central point of the update was video playback. The most important finding: no fetch, no pre-check, no redirect logic. The video element receives the URL directly – the browser does the rest.
video.src = 'https://access.allsky-rodgau.de/indi-allsky/latesttimelapse?night=' + night + '&ts=' + Date.now();
Chromium follows the redirect automatically and plays the MP4.
Catching errors cleanly
If no video exists, there is no “trial and error”, but a clear status is displayed.
video.addEventListener('error', () => { message.style.display = 'flex'; });This prevents black screens and makes it immediately clear what is going on.
The entire logic is deliberately limited to a few events:
video.addEventListener('canplay', () => { container.classList.add('visible'); }); video.addEventListener('error', () => { message.style.display = 'flex'; });Navigation back to the dashboard is always via the same ✕ symbol:
function goBack() { window.location.href = 'index.html'; }timelapse.html (complete, final)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=1280, height=800, initial-scale=1.0, user-scalable=no">
<title> </title>
<style>
html, body {
margin: 0;
padding: 0;
width: 1280px;
height: 800px;
background: #000;
font-family: Arial, Helvetica, sans-serif;
overflow: hidden;
color: #fff;
}
.container {
position: relative;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.8s ease;
}
.container.visible {
opacity: 1;
}
video {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.message {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
font-size: 22px;
color: #ccc;
text-align: center;
}
.close {
position: fixed;
top: 20px;
right: 20px;
width: 48px;
height: 48px;
font-size: 32px;
line-height: 48px;
text-align: center;
background: rgba(0,0,0,0.6);
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="close" onclick="goBack()">✕</div>
<div class="container" id="container">
<video id="video"
autoplay
muted
playsinline>
</video>
<div class="message" id="message">
No time lapse available
</div>
</div>
<script>
const params = new URLSearchParams(window.location.search);
const night = params.get('night');
const video = document.getElementById('video');
const container = document.getElementById('container');
const message = document.getElementById('message');
video.addEventListener('canplay', () => {
container.classList.add('visible');
});
video.addEventListener('error', () => {
message.style.display = 'flex';
container.classList.add('visible');
});
video.src =
'https://access.allsky-rodgau.de/indi-allsky/latesttimelapse?night=' +
night + '&ts=' + Date.now();
function goBack() {
container.classList.remove('visible');
setTimeout(() => {
window.location.href = 'index.html';
}, 800);
}
</script>
</body>
</html>
Full screen view for the live image: fullscreen.html
The current Allsky image can be displayed in full screen – without browser UI, without navigation elements.
<img id="latestImage" src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT">
Automatic update:
setInterval(() => { latestImage.src = 'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now(); }, 25000);fullscreen.html (complete, final)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=1280, height=800, initial-scale=1.0, user-scalable=no">
<title> </title>
<style>
html, body {
margin: 0;
padding: 0;
width: 1280px;
height: 800px;
background: #000;
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.close {
position: fixed;
top: 20px;
right: 20px;
width: 48px;
height: 48px;
font-size: 32px;
line-height: 48px;
text-align: center;
background: rgba(0,0,0,0.6);
color: #fff;
cursor: pointer;
border-radius: 4px;
}
.dashboard {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
background: rgba(0,0,0,0.6);
color: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
}
</style>
</head>
<body>
<img id="latestImage"
src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT">
<div class="close" onclick="goDashboard()">✕</div>
<div class="dashboard" onclick="goDashboard()">Dashboard</div>
<script>
setInterval(() => {
latestImage.src =
'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now();
}, 25000);
function goDashboard() {
window.location.href = 'index.html';
}
</script>
</body>
</html>
