Family Dashboard v3.30: Extreme Weather Alerts, Temperature Shifts, and Birthday Detection

Family Dashboard wall display showing weather, calendar, and tide data on an iPad
Updated April 13, 2026: This post has been updated to include the v3.30.1 and v3.30.2 releases. The Weather Narrative Engine section covers the v3.30.2 "Clean Weather Commentary" content audit and the multi-factor scoring system introduced in this release.
TL;DR: Family Dashboard v3.30 adds extreme weather overlays (orange/red/rainbow alerts for NOAA warning levels), temperature shift alerts when the forecast swings ≥15°F from today's high, birthday and holiday detection with a 30-day calendar scan, support for up to three simultaneous CalDAV accounts with per-account color coding, and a modular architecture overhaul that shrunk the bundle from 160 KB to 68 KB. The open-source repo is live and has 3 community forks.

Where We Left Off

The previous post covered v3.7 through v3.26: the move from OAuth to CalDAV, the removal of 2,000+ lines of dead code, and the introduction of personality-driven weather commentary. At v3.26 the dashboard was reliable and pleasant to use — but it was missing something.

A wall display that silently shows the wrong temperature because a cold front moved in overnight isn't doing its job. Neither is one that shows "Happy Tuesday" when a tornado watch is active two counties over. The gap between "it's currently 58°F" and "it was supposed to be 75°F today" is real information. v3.27 through v3.30 plugged these gaps.

Extreme Weather Alert Overlays (v3.29)

The dashboard already showed current conditions and forecasts, but it treated a sunny afternoon and an active severe thunderstorm warning the same way: a weather icon and a number. That's fine 355 days a year. On the other 10, you want the display to scream.

v3.29 introduced a tiered visual alert system keyed to NOAA severity levels:

  • Warning level — Orange pulsing border overlay. Something significant is happening nearby; pay attention.
  • Severe level — Red full-screen overlay with high-contrast text. Take shelter.
  • Extreme level — Animated rainbow border overlay. This is the one you won't miss from across the kitchen.

The alert state is evaluated on every weather refresh. When the NOAA API returns a warning event for the configured location, the overlay renders over the normal dashboard content. When the event expires or is lifted, the overlay clears automatically.

function applyWeatherAlertOverlay(alertLevel) {
    const overlay = document.getElementById('alert-overlay');
    overlay.className = ''; // reset

    if (alertLevel === 'extreme') {
        overlay.classList.add('alert-extreme'); // animated rainbow
    } else if (alertLevel === 'severe') {
        overlay.classList.add('alert-severe');  // red full-screen
    } else if (alertLevel === 'warning') {
        overlay.classList.add('alert-warning'); // orange pulsing border
    }

    overlay.hidden = alertLevel === null;
}

The overlay approach — a dedicated element that sits above the dashboard content — means the underlying data remains accurate and visible. The alert is additive, not a replacement. A severe storm warning shows the storm warning and the current 52°F temperature, which is what you actually need if you're trying to decide whether to bring in the patio furniture.

Temperature Shift Alerts (v3.30)

The extreme weather overlays handle the rare case. Temperature shift alerts handle the common one: the forecast that changed since you last looked.

The trigger is a ≥15°F deviation between this morning's forecast high and the current forecast high. If you woke up to a "75°F and sunny" forecast and by noon the model is showing 55°F, the dashboard flags it:

function checkTemperatureShift(forecastHighAtPageLoad, currentForecastHigh) {
    const shift = Math.abs(currentForecastHigh - forecastHighAtPageLoad);

    if (shift >= 15) {
        const direction = currentForecastHigh > forecastHighAtPageLoad ? 'warmer' : 'cooler';
        renderTemperatureShiftAlert(shift, direction);
    }
}

The shift alert appears as a banner in the weather section rather than a full-screen overlay — it's informational, not urgent. It shows the original forecast, the current forecast, and the delta. For a family that plans outdoor activities in the morning based on the day's forecast, this has been one of the most practically useful additions.

The 15°F Threshold

Why 15°F? Because smaller swings happen constantly and don't change plans. A 3°F revision from 72°F to 69°F is noise. A 15°F swing from 75°F to 60°F crosses the jacket line, the "do we cancel the beach trip" line, the "kids need different clothes" line. The number came from a few weeks of watching the dashboard and counting the times a smaller threshold would have triggered alerts that didn't change anyone's behavior. 15°F was the threshold where the alert reliably meant something.

Birthday and Holiday Detection (v3.30)

The calendar integration already showed today's events and the next two days. v3.30 added a separate scan layer that looks 30 days out specifically for birthdays and holidays — the calendar events that you don't notice are coming until the day of.

The detection logic runs a VEVENT scan across all connected CalDAV accounts on each calendar refresh. It looks for two patterns:

  • Birthdays — Events with "birthday" in the summary (case-insensitive), recurring annually
  • Holidays — Events from dedicated holiday calendars (detected by the calendar's display name or X-WR-CALNAME property)
function scanForSpecialEvents(events, lookAheadDays = 30) {
    const cutoff = addDays(new Date(), lookAheadDays);
    const specials = [];

    for (const event of events) {
        const isBirthday = /birthday/i.test(event.summary);
        const isHoliday  = event.calendarType === 'holiday';

        if ((isBirthday || isHoliday) && event.startDate <= cutoff) {
            specials.push({
                type: isBirthday ? 'birthday' : 'holiday',
                summary: event.summary,
                daysUntil: daysBetween(new Date(), event.startDate)
            });
        }
    }

    return specials.sort((a, b) => a.daysUntil - b.daysUntil);
}

Detected birthdays and holidays appear in a dedicated section at the bottom of the calendar panel, sorted by proximity. An event three days out gets more visual weight than one 25 days out. The section is hidden entirely when there's nothing in the 30-day window — no empty state, no "no upcoming birthdays" message. The absence of the section is the information.

Multi-Account CalDAV (v3.28)

v3.26 supported one CalDAV account at a time — enough for a single person's calendar or a shared family calendar. The real scheduling picture for most families spans multiple accounts: one per family member, plus a shared household calendar.

v3.28 expanded support to three simultaneous CalDAV accounts. Each account is configured independently through the setup page, and each gets its own color in the event display:

// config.js — stored in LocalStorage
const calendarAccounts = [
    { email: 'parent1@gmail.com', password: '...', color: '#00d4ff', label: 'Mom' },
    { email: 'parent2@gmail.com', password: '...', color: '#7c3aed', label: 'Dad' },
    { email: 'family@shared.com', password: '...', color: '#f59e0b', label: 'Family' }
];

The refresh cycle fires all three CalDAV requests in parallel and merges the results into a single sorted event list. Events from different accounts with the same time slot don't conflict — they stack, with their per-account color making the source immediately visible. A 6 PM event in cyan is Mom's; a 6 PM event in purple is Dad's. Scheduling conflicts become visible at a glance.

Each account also gets its own connection status indicator on the setup page. If the third account's CalDAV server returns an error, the other two still show their events. The failure is isolated.

Bundle Optimization: 160 KB → 68 KB

One of the less visible but more consequential changes across v3.27–v3.30 was a systematic modularization pass that reduced the JavaScript bundle from ~160 KB to ~68 KB — a 57% reduction.

The old architecture was a sprawling single-file dashboard where utilities, API clients, and rendering logic were interleaved. The refactor extracted five dedicated modules:

  • logger.js — Centralized logging with configurable verbosity levels. Debug output stays out of production.
  • error-handler.js — Unified error capture, categorization, and retry scheduling with exponential backoff.
  • date-utils.js — All timezone arithmetic, DST handling, and date formatting in one place. No more scattered new Date() calls.
  • weather-narrative-engine.js — The weather commentary system, separated from the rendering layer that calls it.
  • api-client.js — Fetch wrapper with AbortController timeouts and automatic retry logic.

Modularization alone doesn't shrink a bundle. The size reduction came from eliminating duplicate logic that had evolved in different parts of the single-file version. The same date arithmetic existed in three places; the same fetch pattern in four. The modules forced a single authoritative implementation of each concern, and the duplicates were deleted.

The Service Worker

The modular architecture also made it practical to implement a service worker for offline capability. A single 160 KB file with tangled dependencies is a poor candidate for granular caching. The five-module structure made the caching strategy clear:

  • Core modules (date-utils.js, logger.js, error-handler.js) — cache-first, long TTL
  • API client and weather engine — cache-first, short TTL
  • Dynamic data (weather, calendar) — network-first, fallback to cache

On a flaky WiFi connection — which describes every old iPad on a home network — this means the dashboard shell loads immediately from cache, then updates its data sections as network requests complete. The display is never blank.

Weather Narrative Engine: The 56 One-Liners (v3.30.2)

The weather commentary system has been through several content revisions since v3.26 introduced 36 messages split between "good weather" and "poor weather" pools. v3.30.0 expanded the library; v3.30.2 did a full content audit.

The v3.30.2 cleanup removed all comedian-inspired quotes — lines that imitated specific comedic styles or referenced pop culture in ways that felt dated or out of place on a family kitchen display. What remained were 56 original one-liners written specifically for the context: something a family of four glances at first thing in the morning while making coffee.

A few examples from the current pool:

  • Good weather: "The forecast is in: go touch some grass." / "Sun's out. The couch can wait." / "Even your shadow is having a good day."
  • Poor weather: "Somewhere out there, a duck is thriving." / "Great conditions for staring dramatically out a window." / "The umbrella has been patiently waiting for this moment."

The selection logic was also refined. The threshold moved from a hard 60°F / no-precipitation binary to a multi-factor score:

function selectWeatherNarrative(temp, precipitation, conditions) {
    const score =
        (temp >= 65 ? 2 : temp >= 50 ? 1 : 0) +
        (precipitation.probability < 20 ? 2 : precipitation.probability < 50 ? 1 : 0) +
        (['Clear', 'Sunny', 'Partly Cloudy'].includes(conditions) ? 1 : 0);

    // score 0-2: poor weather pool; 3-5: good weather pool
    const pool = score >= 3 ? goodWeatherNarratives : poorWeatherNarratives;
    return pool[Math.floor(Math.random() * pool.length)];
}

A 63°F overcast day with 40% precipitation probability scores a 2 — poor weather commentary. A 70°F sunny day with 10% precipitation scores a 5 — good weather commentary. The previous version treated the same 63°F overcast day as "good weather" because the temperature crossed 60°F. The scoring approach is closer to how people actually experience a day.

NOAA Tide Integration with Station Fallbacks

Tide data was in the dashboard from v1, but the station handling was fragile: a single NOAA station ID, and if that station's data was unavailable, the tide panel showed an error.

v3.29 added a fallback chain. The configuration now accepts a primary station and up to two alternates. On each tide refresh, the client tries the primary station first. If the request fails or returns no data, it falls through to the first alternate, then the second:

async function fetchTideData(stations) {
    for (const stationId of stations) {
        try {
            const data = await fetchNOAAStation(stationId);
            if (data?.predictions?.length) return data;
        } catch {
            // try next station
        }
    }
    return null; // all stations failed
}

For coastal families where tide timing affects plans — fishing, beach access, kayaking windows — the fallback chain means one NOAA station going offline for maintenance doesn't blank the tide display. A nearby station covers the gap with close-enough predictions.

What v3.30.2 Looks Like in Production

The dashboard that's been running in the kitchen since the v3.30.2 deploy:

  • Three CalDAV accounts refreshing in parallel every 5 minutes, events color-coded by family member
  • 30-day birthday/holiday scan surfacing upcoming events two weeks before they'd appear in the daily view
  • Weather narrative that genuinely gets the tone right more often than not
  • Temperature shift banner that fires a few times a month when the forecast changes meaningfully
  • Weather alert overlays that haven't fired yet this year, which is exactly what you want
  • 68 KB total JavaScript, served from cache on every load via the service worker
  • Tile data from the primary NOAA station with two fallbacks configured

The hardware is still the same retired iPad from v1. Web app, Safari, added to home screen for full-screen kiosk mode. No app updates to manage, no OS compatibility issues to chase.

What's Open on GitHub

The family-dash repo is public and MIT-licensed. Three community forks have appeared since the CalDAV work landed — someone added a fourth calendar account slot, someone adapted the layout for a landscape TV display, and one fork is experimenting with Home Assistant integration for the weather data source.

Setup requires an OpenWeatherMap API key (free tier handles the load easily) and, for calendar integration, CalDAV credentials. Google Calendar requires an App Password, which takes about two minutes to generate from Google Account settings. Full setup instructions are in the README.

If you have a tablet gathering dust and a family that keeps asking "is it going to rain?", this is a Saturday afternoon project.

Want a Custom Dashboard for Your Home or Business?

From wall-mounted family displays to internal operations dashboards, I build real-time data views that integrate with your existing calendars, APIs, and data sources.