{"slug": "building-status-live-ski-lifts-roads-and-webcams-with-visible-data-freshness", "title": "Building status.live: ski lifts, roads, and webcams with visible data freshness", "summary": "A developer built status.live, a free site that aggregates ski lift status, road conditions, webcams, park alerts and surf conditions across 284 ski resorts, exposing the data through a read-only JSON API and MCP tools. The implementation distinguishes reported closures from empty or failed sources, tracks observation time separately from check time, and uses scheduled ingestion on Cloudflare Workers with KV snapshots and D1 to keep upstream calls at 432 per day rather than scaling with traffic.", "body_md": "A ski lift can be running while the access road is restricted. A webcam can respond successfully while showing an old image. An empty lift board can mean the resort has stopped publishing its winter report.\n\nThese are different states, but a simple “open” or “closed” badge can hide the distinction.\n\nI’m building [status.live](https://status.live/), a free website for checking ski lifts, road conditions, webcams, park alerts, campground opening schedules, and surf conditions. It also exposes a read-only JSON API and MCP tools. The ski catalog currently includes 284 resorts; available data and freshness vary by source.\n\nHere are four decisions that have shaped the implementation, including a map mistake that made an otherwise valid build hard to use.\n\nA successful HTTP response doesn’t necessarily contain a useful lift report. A parser can return an empty list because the resort has removed its seasonal board, changed its markup, or genuinely has nothing listed.\n\nThe data model needs to preserve that uncertainty. A simplified version looks like this:\n\n```\ntype LiveSection<T> = {\n  state: 'reported' | 'none_listed' | 'source_error' | 'loading';\n  as_of: string | null;\n  age_s: number | null;\n  stale: boolean;\n  items: T[];\n  source: { name: string; url?: string };\n};\n```\n\nThis is an illustrative contract rather than a complete response schema. The important part is that an empty collection and a failed source read are distinguishable from a reported closure.\n\nThere are also two useful times to keep separate: when a source says an event happened, and when the application successfully checked it. Refreshing an HTML page should not make an old upstream observation look new.\n\nWhen a source fails, a last-good snapshot can still be useful. It needs to retain its timestamp and carry a stale flag. Once that snapshot is too old to be useful, the interface should expose the missing data.\n\nFetching every source while rendering every page makes upstream traffic depend on how popular the site becomes.\n\nFor Colorado roads, the scheduled ingestion path is:\n\n``` php\nCDOT / COtrip statewide feeds\n    -> scheduled ingestion\n    -> shared snapshots\n    -> resort pages, park pages, JSON API, MCP\n```\n\nThe normal schedule reads three feeds—road conditions, incidents, and planned events—once every ten minutes. That is 432 scheduled upstream calls per day, rather than three calls for every visitor or resort page.\n\nA shared cooldown handles HTTP 429 responses and respects Retry-After. Failed refreshes preserve last-good data with its original timestamp. Visitors read the stored snapshots rather than independently retrying the upstream service.\n\nOther sources use caches suited to their update frequency and terms. The application runs on Cloudflare Workers, with KV for shared snapshots and D1 for catalog and monitoring records. A global cache matters here: a cache confined to one data center can still produce repeated upstream reads from different regions.\n\nLift status, roads, weather, and webcams have different sources and refresh cycles. A weather-service timeout should not erase an available lift report.\n\nThe page and API keep those sections separate. A section can be loading or unavailable while other sections remain useful. Each live section carries its own freshness information; one timestamp for the entire page would conceal those differences.\n\nThe distinctions also extend to what the data means:\n\nThe same principle applies to an AI assistant consuming the MCP tools. The assistant needs the source and uncertainty, not just a number it can confidently repeat.\n\nThe resort maps combine OpenStreetMap runs and lifts with elevation and, where available, reusable aerial imagery. The Swiss example below credits swisstopo, Copernicus EU-DEM, and OpenStreetMap contributors. It is a generated map, not the resort’s official piste map.\n\nGstaad exposed a usability problem. It spans separate ski sectors, and choosing orientation from the aggregate direction of the lifts put the main village toward the top. The terrain and lines rendered correctly, but the map was difficult to understand.\n\nThe revised 2D map places Gstaad village in the lower part, marks it explicitly, and labels the separate sectors. The build now checks the village’s vertical position when a village marker is configured.\n\n*Gstaad’s three main mapped ski areas. Glacier 3000 and the peripheral lifts on its broader status board are outside this map; the resort page also links to the official map.*\n\nThat review has become part of adding each new resort: verify boundaries, compare lift names, check the village position, and inspect labels at the size readers will actually see. Some resort pages still have official-map links while their generated maps are being added.\n\nFor a concrete API example:\n\n```\ncurl --fail --silent \\\n  'https://status.live/api/v1/ski/resorts/palisades?wait_ms=1500'\n```\n\nInspect each section’s state and freshness before using its values. With a response time budget, slower sections may still be loading.\n\nThe [API reference](https://status.live/api) includes interactive examples, and the [source-health page](https://status.live/status) shows the monitoring side of the project. For AI clients, setup instructions are at [status.live/connect](https://status.live/connect).\n\nThe website requires no account. Personal and non-commercial API use is free; reuse and source-credit requirements are described in the [terms](https://status.live/terms).\n\nIf you build with live outdoor data, I’d be interested in how you present stale information without making people dig for it. Feedback on the resort pages—especially unclear states or map labels—is welcome too.", "url": "https://wpnews.pro/news/building-status-live-ski-lifts-roads-and-webcams-with-visible-data-freshness", "canonical_source": "https://dev.to/elvean/building-statuslive-ski-lifts-roads-and-webcams-with-visible-data-freshness-4jk6", "published_at": "2026-10-24 18:33:45+00:00", "updated_at": "2026-10-24 18:44:45.130445+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "agent-protocols", "ai-infrastructure"], "entities": ["status.live", "Cloudflare Workers", "Cloudflare KV", "Cloudflare D1", "OpenStreetMap", "swisstopo", "Copernicus EU-DEM", "Gstaad"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/building-status-live-ski-lifts-roads-and-webcams-with-visible-data-freshness", "markdown": "https://wpnews.pro/news/building-status-live-ski-lifts-roads-and-webcams-with-visible-data-freshness.md", "text": "https://wpnews.pro/news/building-status-live-ski-lifts-roads-and-webcams-with-visible-data-freshness.txt", "jsonld": "https://wpnews.pro/news/building-status-live-ski-lifts-roads-and-webcams-with-visible-data-freshness.jsonld"}}