A Connected Calendar Dashboard for My Grandmother

by ArthurB24 in Circuits > Raspberry Pi

81 Views, 0 Favorites, 0 Comments

A Connected Calendar Dashboard for My Grandmother

IMG_20260524_150245918.jpg
IMG_20260524_150540747.jpg

My grandmother is no longer very independent in her daily life and frequently gets confused about her schedule. To help her, my mother handles entering her medical appointments and visits into a Google Calendar. Initially, a cheap Android tablet was mounted on the wall for display, but my grandmother would invariably touch the touchscreen by mistake, closing the application without knowing how to get back to the original screen.


To overcome this and provide optimal visual comfort, I designed a custom system: a 24-inch desktop monitor (much more readable than a 10" tablet) connected to a Raspberry Pi 3B.


The goal: display a single HTML page in full screen using Chromium's Kiosk mode. This dashboard displays the time in a large font, the Google Calendar via an Iframe, and a dedicated space for affectionate messages that the family can send remotely. Let's get to work!

Supplies

As hardware for this project, on only used :

  1. an old Raspberry Pi 3B : https://www.amazon.com/Raspberry-Pi-MS-004-00000024-Model-Board/dp/B01LPLPBS8
  2. a spare 24" monitor


For the development on my personnal computer, I used Visual Studio Code.

Firebase Configuration: the Heart of the System

To power this project, I chose Firebase (Google's Cloud platform). It offers three free tools that are essential for our use case: Hosting (to host the family's application), Authentication (to secure access), and the Realtime Database (to send messages instantly without reloading the page).


Project Architecture

To fully understand what follows, we must distinguish between two entities that share the same Firebase database but live in different places:


[ Family Smartphone ] ---> ( Hosted on Firebase Hosting )
[ FIREBASE DATABASE ] <─── (Anonymous Internet Connection) ─── [ Raspberry Pi (Local) ]


The Input Application (The Family Form):

Where is it? It is hosted online on Firebase Hosting servers.

Why? So that any member of the family can access it from their smartphone or computer, anywhere in the world, via a secure URL address (https://...). This is the part that requires strict authentication.


The Dashboard (Granny's Screen):

Where is it? It is not hosted on the Internet. It is a simple index.html file (along with its CSS and JavaScript) stored locally, directly in the Raspberry Pi's memory.

Why? The Raspberry Pi simply runs a web browser (Chromium) that opens this local file at startup. This HTML file contains the Firebase script that connects via the Internet to the database to "listen" for changes in real-time. If the internet connection drops temporarily, the local interface remains displayed.


In summary:

Firebase serves as a web server only for the family form. For the Raspberry Pi, Firebase acts solely as a data gateway. The local Dashboard simply reads information from the Google Calendar and the messages pushed by Firebase.

Project Creation and Declaration

It all starts on the Firebase Console.

Creation: Click on "Add project", name it (for example agenda-mamie) and disable Google Analytics (unnecessary for this niche project).

Application Declaration: Once the project is ready, create a Web application by clicking on the </> icon. Give it a name and check the box "Also set up Firebase Hosting".

Configuration keys: Firebase then generates a JavaScript object containing your credentials (API Key, Project ID, etc.). This block of code needs to be copied and pasted into our HTML file to initialize it and link it to our database.

Authentication Configuration

To prevent pranksters from sending messages to my grandmother's screen, access to the form must be restricted to the family.

In the left menu, go to Authentication > Get Started.

Enable the Email/Password provider.

Go to the Users tab and click on "Add user". This is where I manually create accounts (email + strong password) for my mother, my cousins, and myself. No need for a public registration form; the family circle is closed.

Realtime Database Deployment

This is Firebase's magical tool. Unlike a traditional database, as soon as data changes on the Cloud side, it is instantly "pushed" to the Raspberry Pi.

In the menu, click on Realtime Database > Create Database.

Choose the server location (Europe-West is preferred for latency).

Security Rules (Crucial): By default, Firebase locks everything or leaves everything open. For our project, we will configure the rules so that:

The Raspberry Pi (the Dashboard) can read messages without being logged in (Public Read).

Only authenticated family members can write (Modify) messages.


Here are the JSON rules to apply:

{
"rules": {
"canal": {
"messages": {
".read": true,
".write": "auth != null"
}
}
}
}

The data structure itself is ultra-simple and generates automatically upon the very first submission: a root node named canal, which contains a messages node, where we store the text, the author, and the date.

The Message Sending Application

Capture d&rsquo;écran 2026-05-19 à 13.44.38.png

I created a very simple, responsive single-page web application so the family can easily use it from their smartphones. To keep the project clean, dependency-free, and easy to maintain, the entire application is contained within a single `index.html` file using vanilla CSS for the layout and the modern Firebase v9 Modular SDK JavaScript syntax.


Here is a detailed breakdown of how the source code works across its three main components.


1. The Dynamic UI and Authentication (HTML & JS)

The application features a secure entryway. In the HTML body, the interface is split into two main sections: `#login-box` and `#message-box`. By default, the message container is explicitly set to `style="display:none;"`. The layout visibility is reactively managed by the Firebase Auth lifecycle listener `onAuthStateChanged`. When a family member successfully logs in, the login box vanishes to make room for the control panel.


// Import modern Firebase v9 modules
import { initializeApp } from '[https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js](https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js)';
import { getAuth, signInWithEmailAndPassword, onAuthStateChanged } from "[https://www.gstatic.com/firebasejs/9.22.0/firebase-auth.js](https://www.gstatic.com/firebasejs/9.22.0/firebase-auth.js)";

// [...] Firebase Config Initialization

const auth = getAuth();

// Sign-In execution linked to the button's click event
window.login = async function () {
const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (error) {
alert("Identifiants invalides.");
}
}

// Reactively toggle UI boxes based on authentication state
onAuthStateChanged(auth, user => {
const loginBox = document.getElementById("login-box");
const messageBox = document.getElementById("message-box");

if (user) {
loginBox.style.display = "none";
messageBox.style.display = "block";
} else {
loginBox.style.display = "block";
messageBox.style.display = "none";
}
});


2. Pushing Messages and Appending History

Instead of simply overwriting a single text entry, this code takes a safer and more scalable approach by using the push() method. Every time the form is submitted, Firebase automatically generates a unique time-stamped ID node inside canal/messages. This prevents concurrent write collisions if multiple family members send a message at the same time.


import { getDatabase, ref, push } from '[https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js](https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js)';

const db = getDatabase(app);
const messagesRef = ref(db, 'canal/messages');

document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('form');
form.addEventListener('submit', async (e) => {
e.preventDefault();

const author = form.author.value.trim();
const text = form.text.value.trim();
const date = new Date().toISOString(); // Tracks the exact local date/time

if (!author || !text) return alert("Tous les champs sont obligatoires");

// Creates a unique entry under canal/messages/
await push(messagesRef, {
author,
text,
date
});

alert('Message envoyé ✅');
form.reset();
});
});


3. Displaying the Live Dashboard Preview and Global Wipeout

To provide immediate feedback, the app displays a preview of the message currently broadcasted to the dashboard. The code sets up a reactive query that isolates the single newest entry (limitToLast(1)).

Additionally, a maintenance button is included to clear up the database feed using the remove() method.


import { query, limitToLast, orderByChild, onValue, remove } from '[https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js](https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js)';

// Query to pull only the single most recent message
const orderedMessagesRef = query(ref(db, 'canal/messages'), orderByChild('timestamp'), limitToLast(1));

// Sync the preview section dynamically whenever the database updates
onValue(orderedMessagesRef, snapshot => {
const messagesContainer = document.getElementById('last-messages');
messagesContainer.innerHTML = ''; // Clear container before updating

const messages = [];
snapshot.forEach(childSnapshot => {
messages.push(childSnapshot.val());
});

// Display the latest entry with author formatting and localized timestamp
messages.reverse().forEach(message => {
const li = document.createElement('li');
const date = new Date(message.date).toLocaleString('fr-FR', { dateStyle: 'short', timeStyle: 'short' });
li.innerHTML = `<strong>${message.author}</strong> (${date})<br>${message.text}`;
messagesContainer.appendChild(li);
});
});

// Admin feature to wipe the canal completely
window.clearMessages = async function () {
if (!confirm("Es-tu sûr de vouloir effacer tous les messages ?")) return;

try {
await remove(ref(getDatabase(), "canal/messages"));
alert("Tous les messages ont été effacés !");
} catch (error) {
console.error("Erreur lors de la suppression :", error);
alert("Une erreur est survenue.");
}
};


Enhanced Security Note: Even if a malicious user inspects the browser element, alters the CSS stylesheets, or manually edits the DOM to bypass the login curtain (display: block), the system remains protected. Firebase handles operations server-side. The database security rules (".write": "auth != null") will check the underlying security token during any push() or remove() attempt, instantly rejecting unauthenticated requests.


Deployment with Firebase Hosting

Thanks to the Firebase command-line tools (firebase-tools), linking the local development directory to the cloud infrastructure is seamless. After configuring your local environment using firebase init, pushing this specific code to production requires just a single terminal command: firebase deploy.


Within seconds, the deployment engine validates the asset folder and generates a public URL hosted on Google's fast global CDN, out of the box with an encrypted, auto-renewing SSL (HTTPS) certificate.


Here's the page code : https://gist.github.com/acalandra/bac10c558b773b9b806a581da8889026

The Dashboard

The Dashboard installed on the Raspberry Pi runs entirely on a local HTML/CSS/JS file. Unlike the input form, this file is stored directly on the Raspberry Pi's local storage and runs inside Chromium configured in `--kiosk` mode. The interface uses a clean, high-contrast dark layout styled with CSS Flexbox to divide the screen efficiently: the top 33% displays the current time and incoming family messages, while the remaining 67% embeds the scheduling layout.


Here is a breakdown of how the dashboard works across its three key features.


1. Dynamic Local Time and Auto-Refresh System

To keep my grandmother oriented, the local script handles time formatting using JavaScript's native `toLocaleDateString` and `toLocaleTimeString` APIs, updating the display text elements every minute. Furthermore, to prevent the browser tab from crashing due to memory leaks over days of uninterrupted usage, an automatic 12-hour software reload cycle is hardcoded into the script.


// Reload the entire dashboard page every 12 hours to maintain system stability
setInterval(function () {
location.reload();
}, 12 * 60 * 60 * 1000);

const dayOptions = { weekday: 'long', day: 'numeric' };
const monthOptions = { year: 'numeric', month: 'long' };
const timeOptions = { hour: '2-digit', minute: '2-digit' };

// Encapsulated time updates (Executed immediately at boot and then every 60,000ms)
setInterval(function () {
const date = new Date();
document.getElementById("day-of-the-month").textContent = date.toLocaleDateString('fr-FR', dayOptions);
document.getElementById("month-and-year").textContent = date.toLocaleDateString('fr-FR', monthOptions);
document.getElementById("time").textContent = date.toLocaleTimeString('fr-FR', timeOptions);
}, 60000);


2. Reactive Single-Message Filtering (Firebase v9)

On the data-fetching side, the dashboard leverages the modern Firebase v9 Modular SDK syntax. Because the historical log is kept under the canal/messages node, the dashboard sets up an isolated query constraint using limitToLast(1) and listens for structural modifications through the onValue method.

Whenever a family member sends a new entry, the callback function wipes the container clean and safely appends a single rendered <li> card featuring a localized timestamp, the author's name, and the text payload.


import { initializeApp } from "[https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js](https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js)";
import { getDatabase, ref, query, limitToLast, orderByChild, onValue } from "[https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js](https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js)";

// [...] Firebase configuration object

const app = initializeApp(firebaseConfig);
const db = getDatabase(app);

// Fetch only the single latest message from the log database
const orderedMessagesRef = query(ref(db, 'canal/messages'), orderByChild('timestamp'), limitToLast(1));

onValue(orderedMessagesRef, snapshot => {
const messagesContainer = document.getElementById('message-list');
messagesContainer.innerHTML = ''; // Wipe DOM before injection

snapshot.forEach(childSnapshot => {
const message = childSnapshot.val();
const li = document.createElement('li');
const date = new Date(message.date).toLocaleString('fr-FR', dayOptions);
const time = new Date(message.date).toLocaleString('fr-FR', timeOptions);

li.innerHTML = `
<div class='message-wrapper'>
<span class='intro'>le ${date} à ${time},<br><b>${message.author}</b> a écrit :</span>
<br><br>
<span class='message'>${message.text}</span>
</div>`;
messagesContainer.appendChild(li);
});
});


3. Google Calendar Clean Integration

The scheduling module relies on a standard HTML <iframe> pointed directly at the target Google Calendar embed endpoint. To prevent my grandmother from getting confused by interactive web controls, the source URL includes explicit configuration parameters (showPrint=0&showTitle=0&showDate=0&showNav=0&showCalendars=0&showTz=0&showTabs=0&mode=AGENDA) to strips away all interface navigation buttons, tabs, headers, and views, keeping only a clean, scroll-locked timeline layout.


<div class="agenda">
<iframe src="[https://calendar.google.com/calendar/embed?height=600&wkst=2&ctz=Europe%2FParis&showPrint=0&showTitle=0&showDate=0&showNav=0&showCalendars=0&showTz=0&showTabs=0&mode=AGENDA&src=your_agenda_id@gmail.com&color=%23F4511E](https://calendar.google.com/calendar/embed?height=600&wkst=2&ctz=Europe%2FParis&showPrint=0&showTitle=0&showDate=0&showNav=0&showCalendars=0&showTz=0&showTabs=0&mode=AGENDA&src=your_agenda_id@gmail.com&color=%23F4511E)"
style="border-width:0"
width="100%"
height="100%"
frameborder="0"
scrolling="no">
</iframe>
</div>


The Raspbian & Chromium Technical Challenges

One of the major road blocks faced during this project was running a sufficiently up-to-date version of Chromium (the open-source engine powering Google Chrome) on the Raspberry Pi 3B hardware architecture.

Older legacy builds of Raspberry Pi OS (Raspbian) are bundled with obsolete versions of Chromium. For security and cross-origin tracking reasons, Google Calendar's embed endpoints implement modern cookie policies and security protocols that completely block execution on outdated engines, preventing the calendar from rendering.

To bypass this hurdle, I had to upgrade the Raspberry Pi OS to its latest stable major release. Following the OS upgrade, I had to manually fetch and configure a backported, hardware-accelerated build of Chromium capable of validating Google's security handshakes and managing the modern JavaScript syntax required by the Firebase v9 Web SDK.

Once these software dependencies were met, the browser was able to parse the local file, connect to the Firebase streaming web-sockets effortlessly, and securely execute the embedded Google calendar view.


Here's the page code : https://gist.github.com/acalandra/bc69722a439e2881d0e98458acad6b4d

Conclusion

The system now runs 24/7. The Raspberry Pi is configured to boot directly into Chromium in kiosk mode (full screen, without an address bar or mouse cursor).

The impact was immediate: my grandmother no longer needs to interact with technology; she only has to look up to see her day displayed in a large format, and she loves seeing the little messages of affection from her grandchildren appear throughout the day!