Bridge
A personal operations dashboard managing chores, fitness, plants, and job applications. Deployed on a home server.
A real-time geophysical dashboard surfacing USGS earthquake, volcano, stream gauge, and flood alert data.
Live Demo
Overview
CronosPulse is a four-page public dashboard that surfaces real-time data from three external APIs: USGS Earthquake FDSN, USGS Water Services IV, and NWS CAP alerts. It covers earthquake search, volcano monitoring, stream gauge readings, and flood alerts.
Architecture
Each page is a full-page Livewire 4 component mounted directly to a route. The request path is: Pages\* class → boot() injects a service singleton → the service wraps an HTTP client, caches the response in Redis, and returns typed readonly DTOs from app/Data/ → the component converts DTOs to plain arrays before Livewire serialises state → Blade renders with Alpine.js managing Leaflet maps and Chart.js charts.
All external API base URLs live in config/api.php and are read from .env. No raw API response ever leaves the service layer — every GeoJSON, WaterML-JSON, and CAP alert shape is normalised into a typed DTO (EarthquakeData, VolcanoData, StreamGaugeData, FloodAlertData, SparklineData) before the component sees it.
Service injection uses boot() rather than Livewire's #[Inject] attribute. #[Inject] initialises the property during class construction, which means it is uninitialised on every POST hydration. boot() runs on both the initial render and all subsequent hydrations, making it the correct injection point.
QuakeWatch
Users click a Leaflet map to set a search location, choose a radius and minimum magnitude, and get a paginated table of USGS FDSN events with magnitude, depth, PAGER alert level, felt reports, and formatted local time. Searches save to a SavedEarthquakeSearch record and re-run via a URL-encoded query string.
Timezone display is resolved client-side using tz-lookup, a JavaScript library that returns an IANA timezone string from latitude and longitude without a paid geocoding API. Each map click dispatches wire:dispatch('map-clicked', {lat, lng, timezone}); the component stores the identifier and applies it via EarthquakeData::formattedTime($tz).
EarthquakeQuery::toArray() throws InvalidArgumentException if both maxradiuskm and maxradius are set simultaneously — the USGS FDSN API treats that combination as undefined behaviour. Sorting is done in-memory with PHP's usort() and the spaceship operator on time_ms, magnitude, or depth_km. Pagination is a LengthAwarePaginator built from a sliced PHP array with no database involvement.
VolcanoWatch
All USGS-monitored US volcanoes render on a Leaflet map alongside a filterable, paginated table. A doughnut chart breaks down the current alert level distribution.
The full dataset is cached for 5 minutes; all filtering runs in-memory via Laravel Collection .when() chains. The doughnut chart is built from .countBy('alert_level') on the filtered collection (not the full set), so the chart always reflects what the table shows. alertLevelOrder is a hardcoded array that enforces consistent slice ordering regardless of which levels are present; zero-count slices are omitted. Map markers are stored in Alpine's this.markers object keyed by vnum for O(1) lookup when a table row is clicked.
Leaflet Markercluster race condition
VolcanoWatch exposed a timing issue: Markercluster fires animation callbacks asynchronously after markers are added. When a filter change triggers a Livewire round-trip, Alpine clears the old markers and adds new ones; Livewire's DOM diff may then destroy and recreate the component instance. If the animation callback fires after _map is set to null during that teardown, it throws "Cannot read property 'flyTo' of null." Setting disableClusteringAtZoom: 8 in the markerClusterGroup options eliminates the async callback at the zoom levels where re-renders are likely — at lower zooms, the animation completes before any re-render can fire.
HydroWatch — Stream Gauges
Users select a US state to load active USGS gauge sites on a Leaflet map, colour-coded by gage height band. Clicking a marker loads a 3-day sparkline of streamflow and gage height; readings refresh every 5 minutes via wire:poll.300s.
The USGS Water Services IV API returns one time series per parameter code in a single response. WaterServicesService groups the WaterML-JSON payload by site code and pairs parameter 00060 (streamflow, ft³/s) with 00065 (gage height, ft) into a single StreamGaugeData DTO per site. HTML entities in USGS parameter names (e.g. ft³/s) are decoded with html_entity_decode(..., ENT_QUOTES | ENT_HTML5). State bounding boxes for fitBounds are a hardcoded lookup table in app.js covering all 50 states. Sparklines are cached at 15 minutes, aligned with the USGS reading interval.
isProvisional() returns true if either series carries quality code 'P'; a disclaimer appears in the popup. Sites missing a gage height sort to the bottom via sortByDesc(fn => $site->gageHeightFt ?? -1).
HydroWatch — Flood Alerts
A two-panel layout shows a national paginated list of active NWS alerts alongside a state map of alert zone polygons. Clicking a list item zooms the map to that alert's polygon; clicking a polygon highlights the corresponding list item.
NWSAlertsService::activeFloodAlerts() filters CAP products against a FLOOD_EVENT_TYPES constant of 19 specific product names (Flash Flood Warning, Coastal Flood Advisory, Hydrologic Outlook, etc.), excluding unrelated NWS products like Wind Advisories. FloodAlertData extracts the state code from the first UGC geocode entry by taking the first two characters of the {STATE}{TYPE}{number} pattern. Products with no GeoJSON geometry appear in the list but are excluded from map rendering. Severity sorts via a lookup table (Extreme > Severe > Moderate > Minor > Unknown) rather than relying on feed order. Two independent Redis caches are maintained: nws.flood.alerts.national (full dataset) and nws.flood.alerts.{stateCd} (per-state), both at 300 seconds.
Reactivity model
Livewire owns server-side state; Alpine owns DOM mutations that don't need a server round-trip. The handoff is browser events. A Leaflet map click fires CustomEvent('map-clicked', {lat, lng, timezone}); Livewire listens with @map-clicked.window="$wire.search(...)". After the server responds, $this->dispatch('earthquakes-updated', earthquakes: [...]) passes data to an Alpine listener that updates markers without another round-trip. All map containers carry wire:ignore so Livewire's DOM diffing never touches them and Leaflet instances survive re-renders.
Theming
:root in app.css defines the light palette (off-white, moss green, gold); [data-theme="dark"] overrides every variable. Alpine on <body> reads localStorage on mount and calls document.documentElement.setAttribute('data-theme', theme) on toggle. Tailwind's @theme inline block exposes the variables as utility aliases (bg-surface, text-muted, bg-accent). No Blade or Livewire file contains a hex value — a palette swap requires editing only the two :root/[data-theme="dark"] blocks in app.css.
Explore
A personal operations dashboard managing chores, fitness, plants, and job applications. Deployed on a home server.
A recipe and cookbook manager with fraction-accurate ingredient scaling, star ratings, and cookbook collections.
A standalone PHP 8.2+ library that evaluates whether a recurring rule string is due on a given date — extracted from Bridge and published on Packagist.