commit b0f5316fb360f8b3fe0d027d01394cb96d3c5a0d
Author: Coder Agent <coder@agents.omni>
Date: Mon Apr 20 12:03:37 2026
feat(cal): wire weather widget into brief view
Instantiate WeatherCache in cal app startup and pass it into brief rendering.
Render a today-only weather card using NWS current conditions + short periods.
Task-Id: t-808
diff --git a/Omni/Cal/Web.hs b/Omni/Cal/Web.hs
index e5af9c71..43415efc 100644
--- a/Omni/Cal/Web.hs
+++ b/Omni/Cal/Web.hs
@@ -2,6 +2,14 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
+{- HLINT ignore "Use traverse_" -}
+{- HLINT ignore "Use unless" -}
+{- HLINT ignore "Use guards" -}
+{- HLINT ignore "Avoid lambda" -}
+{- HLINT ignore "Redundant bracket" -}
+{- HLINT ignore "Use tuple-section" -}
+{- HLINT ignore "Replace case with fromMaybe" -}
+
-- | Calendar web viewer.
--
-- Read-only calendar views served at /cal. Reads ICS files from the
@@ -64,7 +72,8 @@ main = do
putText <| "Starting calendar viewer on http://localhost:" <> tshow port
cache <- Parse.newCache
geoCache <- Geo.newGeoCache "/var/fund/geocache.json"
- Warp.run port (app "" calPath cache geoCache)
+ weatherCache <- Weather.newWeatherCache
+ Warp.run port (app "" calPath cache geoCache weatherCache)
-- | Create an app with a warmed cache. For use when mounting in Omni.Web.Core.
-- Loads and caches all calendar events on startup so the first page load is fast.
@@ -72,17 +81,18 @@ mkApp :: FilePath -> IO (Text -> Wai.Application)
mkApp calPath = do
cache <- Parse.newCache
geoCache <- Geo.newGeoCache "/var/fund/geocache.json"
+ weatherCache <- Weather.newWeatherCache
-- Warm the cache: load events for a wide range so first request is instant
let sources = Parse.defaultSources calPath
now <- Time.getCurrentTime
let warmStart = Time.addDays (-30) (Time.utctDay now)
warmEnd = Time.addDays 60 (Time.utctDay now)
_ <- Parse.loadEventsInRange cache sources warmStart warmEnd
- pure (\basePath -> app basePath calPath cache geoCache)
+ pure (\basePath -> app basePath calPath cache geoCache weatherCache)
-- | WAI application for the calendar viewer.
-app :: Text -> FilePath -> Parse.CalendarCache -> Geo.GeoCache -> Wai.Application
-app basePath calPath cache geoCache req respond = do
+app :: Text -> FilePath -> Parse.CalendarCache -> Geo.GeoCache -> Weather.WeatherCache -> Wai.Application
+app basePath calPath cache geoCache weatherCache req respond = do
let path = Wai.pathInfo req
query = Wai.queryString req
sources = Parse.defaultSources calPath
@@ -94,7 +104,7 @@ app basePath calPath cache geoCache req respond = do
let endDay = Time.addDays 7 fromDay
historyStart = Time.addDays (-28) fromDay
events <- Parse.loadEventsInRange cache sources historyStart endDay
- briefHtml <- briefPage basePath sources now fromDay events geoCache
+ briefHtml <- briefPage basePath sources now fromDay events geoCache weatherCache
respond <| htmlResponse briefHtml
["day"] -> do
let endDay = Time.addDays 7 fromDay
@@ -136,7 +146,7 @@ app basePath calPath cache geoCache req respond = do
let endDay = Time.addDays 7 fromDay
historyStart = Time.addDays (-28) fromDay
events <- Parse.loadEventsInRange cache sources historyStart endDay
- briefHtml <- briefPage basePath sources now fromDay events geoCache
+ briefHtml <- briefPage basePath sources now fromDay events geoCache weatherCache
respond <| htmlResponse briefHtml
["event", uidParam] -> do
let dateParam = parseQueryText query "date"
@@ -826,8 +836,8 @@ weekGrid now startDay numDays events = do
-- Morning brief view
-- ============================================================================
-briefPage :: Text -> [Parse.CalendarSource] -> Time.UTCTime -> Time.Day -> [Parse.CalEvent] -> Geo.GeoCache -> IO (L.Html ())
-briefPage basePath sources now selectedDay events geoCache = do
+briefPage :: Text -> [Parse.CalendarSource] -> Time.UTCTime -> Time.Day -> [Parse.CalEvent] -> Geo.GeoCache -> Weather.WeatherCache -> IO (L.Html ())
+briefPage basePath sources now selectedDay events geoCache weatherCache = do
-- Geocode selected day's timed events for the map
let dayStart = Parse.easternDayStart selectedDay
dayEnd = Parse.easternDayStart (Time.addDays 1 selectedDay)
@@ -837,6 +847,7 @@ briefPage basePath sources now selectedDay events geoCache = do
indexedLocs = zipWith (\i e -> (i, Parse.ceLocation e, Parse.ceDescription e)) [1 ..] sorted
isToday = selectedDay == Time.utctDay now
geoResults <- Geo.geocodeEvents geoCache indexedLocs
+ weather <- if isToday then Weather.getWeather weatherCache else pure Nothing
pure
<| calShell basePath "Brief"
<| do
@@ -845,6 +856,8 @@ briefPage basePath sources now selectedDay events geoCache = do
L.div_ [L.class_ "cal-brief"] <| do
-- Next up (only when viewing today)
when isToday <| briefNextUp basePath now dayTimed
+ -- Current weather (today only)
+ when isToday <| briefWeather weather
-- Day summary with geo data on each event row
briefDaySummary basePath now selectedDay dayEvents geoResults
-- Day map (container + Leaflet init; markers rebuilt by JS)
@@ -892,6 +905,43 @@ briefNextUp _basePath now timedEvents = do
L.div_ [L.class_ "cal-brief-next-loc"] (L.toHtml loc)
_ -> pure ()
+-- | Current weather card for the brief view.
+briefWeather :: Maybe Weather.CurrentWeather -> L.Html ()
+briefWeather mWeather =
+ L.div_ [L.class_ "cal-brief-section"] <| do
+ L.div_ [L.class_ "cal-brief-label"] "Weather"
+ case mWeather of
+ Nothing ->
+ L.div_ [L.class_ "cal-brief-empty"] "Weather unavailable"
+ Just weather ->
+ L.div_ [L.class_ "cal-brief-weather"] <| do
+ L.div_ [L.class_ "cal-brief-weather-now"] <| do
+ L.span_ [L.class_ "cal-brief-weather-icon"] (L.toHtml (Weather.cwIcon weather))
+ L.div_ [L.class_ "cal-brief-weather-main"] <| do
+ L.div_ [L.class_ "cal-brief-weather-temp"]
+ <| L.toHtml (tshow (Weather.cwTemp weather) <> "°" <> Weather.cwTempUnit weather)
+ L.div_ [L.class_ "cal-brief-weather-forecast"] (L.toHtml (Weather.cwShortForecast weather))
+ L.div_ [L.class_ "cal-brief-weather-range"] <| do
+ case Weather.cwHigh weather of
+ Nothing -> pure ()
+ Just hi -> L.div_ [] (L.toHtml ("H " <> tshow hi <> "°"))
+ case Weather.cwLow weather of
+ Nothing -> pure ()
+ Just lo -> L.div_ [] (L.toHtml ("L " <> tshow lo <> "°"))
+ L.div_ [L.class_ "cal-brief-weather-meta"]
+ <| L.toHtml ("Wind " <> Weather.cwWindDirection weather <> " " <> Weather.cwWindSpeed weather)
+ let periods = take 4 (Weather.cwPeriods weather)
+ when (not (null periods))
+ <| L.div_ [L.class_ "cal-brief-weather-periods"]
+ <| mapM_ renderWeatherPeriod periods
+
+renderWeatherPeriod :: Weather.ForecastPeriod -> L.Html ()
+renderWeatherPeriod period =
+ L.div_ [L.class_ "cal-brief-weather-period"] <| do
+ L.div_ [L.class_ "cal-brief-weather-period-name"] (L.toHtml (Weather.fpName period))
+ L.div_ [L.class_ "cal-brief-weather-period-temp"]
+ <| L.toHtml (Weather.fpIcon period <> " " <> tshow (Weather.fpTemp period) <> "°")
+
-- | Day summary section with geo data attributes for client-side map rendering.
briefDaySummary :: Text -> Time.UTCTime -> Time.Day -> [Parse.CalEvent] -> [(Int, Geo.GeoResult)] -> L.Html ()
briefDaySummary _basePath now selectedDay dayEvents geoResults = do
@@ -2059,6 +2109,64 @@ calStyles = do
Clay.color WebStyle.cFgMuted
Clay.marginTop (Clay.px 4)
+ -- Weather
+ ".cal-brief-weather" Clay.? do
+ Clay.backgroundColor WebStyle.cBgRaised
+ Clay.borderRadius (Clay.px 6) (Clay.px 6) (Clay.px 6) (Clay.px 6)
+ Clay.padding (Clay.px 12) (Clay.px 14) (Clay.px 12) (Clay.px 14)
+
+ ".cal-brief-weather-now" Clay.? do
+ Clay.display Clay.flex
+ Clay.alignItems Clay.center
+ Stylesheet.key "gap" ("10px" :: Text)
+
+ ".cal-brief-weather-icon" Clay.? do
+ Clay.fontSize (Clay.px 26)
+
+ ".cal-brief-weather-main" Clay.? do
+ Stylesheet.key "flex" ("1" :: Text)
+
+ ".cal-brief-weather-temp" Clay.? do
+ Clay.fontSize (Clay.px 24)
+ Clay.fontWeight Clay.bold
+ Clay.color WebStyle.cAccent
+ Clay.lineHeight (Clay.unitless 1.1)
+
+ ".cal-brief-weather-forecast" Clay.? do
+ Clay.fontSize (Clay.px 13)
+ Clay.color WebStyle.cFgMuted
+
+ ".cal-brief-weather-range" Clay.? do
+ Clay.fontSize (Clay.px 12)
+ Clay.color WebStyle.cFgMuted
+ Clay.textAlign Clay.end
+
+ ".cal-brief-weather-meta" Clay.? do
+ Clay.marginTop (Clay.px 6)
+ Clay.fontSize (Clay.px 12)
+ Clay.color WebStyle.cFgMuted
+
+ ".cal-brief-weather-periods" Clay.? do
+ Clay.marginTop (Clay.px 10)
+ Clay.display Clay.flex
+ Stylesheet.key "gap" ("6px" :: Text)
+
+ ".cal-brief-weather-period" Clay.? do
+ Stylesheet.key "flex" ("1" :: Text)
+ Clay.backgroundColor WebStyle.cBgActive
+ Clay.borderRadius (Clay.px 4) (Clay.px 4) (Clay.px 4) (Clay.px 4)
+ Clay.padding (Clay.px 6) (Clay.px 8) (Clay.px 6) (Clay.px 8)
+
+ ".cal-brief-weather-period-name" Clay.? do
+ Clay.fontSize (Clay.px 10)
+ Clay.color WebStyle.cFgFaint
+ Stylesheet.key "text-transform" ("uppercase" :: Text)
+
+ ".cal-brief-weather-period-temp" Clay.? do
+ Clay.fontSize (Clay.px 12)
+ Clay.color WebStyle.cFg
+ Clay.fontWeight Clay.bold
+
-- Event list
".cal-brief-events" Clay.? do
Clay.display Clay.flex