module Main (main) where import Control.Concurrent (threadDelay) import Data.Aeson (FromJSON (..), withObject, (.:)) import Data.ByteString qualified as BS import Data.ByteString.Char8 qualified as BS8 import Data.ByteString.Lazy qualified as LBS import Data.Text (Text) import Data.Text qualified as T import Data.Time.Clock (getCurrentTime) import Data.Time.Format (defaultTimeLocale, formatTime) import Hypermedia.Datastar import Network.HTTP.Types (status200, status404) import Network.Wai (Application, pathInfo, requestMethod, responseLBS) import Network.Wai qualified as Wai import Network.Wai.Handler.Warp qualified as Warp import System.Environment (getArgs) data Signals = Signals { _sInterval :: Int , _sEvents :: Int , _sGenerating :: Bool , _sTotal :: Int , _sDone :: Int , _sWarn :: Int , _sFail :: Int , _sInfo :: Int } instance FromJSON Signals where parseJSON = withObject "Signals" $ \o -> Signals <$> o .: "interval" <*> o .: "events" <*> o .: "generating" <*> o .: "total" <*> o .: "done" <*> o .: "warn" <*> o .: "fail" <*> o .: "info" data Status = Done | Warn | Fail | Info statusFromText :: Text -> Maybe Status statusFromText "done" = Just Done statusFromText "warn" = Just Warn statusFromText "fail" = Just Fail statusFromText "info" = Just Info statusFromText _ = Nothing statusColor :: Status -> Text statusColor Done = "green" statusColor Warn = "yellow" statusColor Fail = "red" statusColor Info = "blue" statusIndicator :: Status -> Text statusIndicator Done = "Done" statusIndicator Warn = "Warn" statusIndicator Fail = "Fail" statusIndicator Info = "Info" eventEntry :: Status -> Int -> Text -> IO Text eventEntry status index source = do now <- getCurrentTime let timestamp = T.pack $ formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S%3Q" now color = statusColor status indicator = statusIndicator status pure $ "
" <> timestamp <> " [ " <> indicator <> " ] " <> source <> " event " <> T.pack (show index) <> "
" main :: IO () main = do args <- getArgs let port = case args of (p : _) -> read p _ -> 3000 htmlContent <- BS.readFile "examples/activity-feed.html" putStrLn $ "Listening on http://localhost:" <> show port Warp.run port (app htmlContent) app :: BS.ByteString -> Application app htmlContent req respond = case (requestMethod req, pathInfo req) of ("GET", []) -> respond $ responseLBS status200 [("Content-Type", "text/html")] (LBS.fromStrict htmlContent) ("POST", ["event", "generate"]) -> handleGenerate req respond ("POST", ["event", statusText]) | Just status <- statusFromText statusText -> handleEvent status req respond _ -> respond $ responseLBS status404 [] "Not found" handleGenerate :: Wai.Request -> (Wai.Response -> IO b) -> IO b handleGenerate req respond = do signalsResult <- readSignals req :: IO (Either String Signals) case signalsResult of Left err -> respond $ responseLBS status404 [] (LBS.fromStrict $ BS8.pack $ "Bad signals: " <> err) Right signals -> respond $ sseResponse nullLogger $ \gen -> do sendPatchSignals gen (patchSignals "{\"generating\": true}") let loop 0 _ _ = pure () loop n total' done' = do let newTotal = total' + 1 newDone = done' + 1 html <- eventEntry Done newTotal "Auto" sendPatchElements gen $ (patchElements html){peSelector = Just "#feed", peMode = After} sendPatchSignals gen $ patchSignals $ "{\"total\": " <> T.pack (show newTotal) <> ", \"done\": " <> T.pack (show newDone) <> "}" threadDelay (_sInterval signals * 1000) loop (n - 1) newTotal newDone loop (_sEvents signals) (_sTotal signals) (_sDone signals) sendPatchSignals gen (patchSignals "{\"generating\": false}") handleEvent :: Status -> Wai.Request -> (Wai.Response -> IO b) -> IO b handleEvent status req respond = do signalsResult <- readSignals req :: IO (Either String Signals) case signalsResult of Left err -> respond $ responseLBS status404 [] (LBS.fromStrict $ BS8.pack $ "Bad signals: " <> err) Right signals -> respond $ sseResponse nullLogger $ \gen -> do let newTotal = _sTotal signals + 1 counterSignals = case status of Done -> "{\"total\": " <> T.pack (show newTotal) <> ", \"done\": " <> T.pack (show (_sDone signals + 1)) <> "}" Warn -> "{\"total\": " <> T.pack (show newTotal) <> ", \"warn\": " <> T.pack (show (_sWarn signals + 1)) <> "}" Fail -> "{\"total\": " <> T.pack (show newTotal) <> ", \"fail\": " <> T.pack (show (_sFail signals + 1)) <> "}" Info -> "{\"total\": " <> T.pack (show newTotal) <> ", \"info\": " <> T.pack (show (_sInfo signals + 1)) <> "}" sendPatchSignals gen (patchSignals counterSignals) html <- eventEntry status newTotal "Manual" sendPatchElements gen $ (patchElements html){peSelector = Just "#feed", peMode = After}