From f10b5fda7f24f72ea51672f64c2d838a58c92b50 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 19 Dec 2025 23:35:21 -0500 Subject: Omni/Ava/Web: refactor to use Servant and Lucid Matches the patterns used in Omni/Jr/Web. --- Omni/Ava/Web.hs | 245 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 122 insertions(+), 123 deletions(-) (limited to 'Omni/Ava/Web.hs') diff --git a/Omni/Ava/Web.hs b/Omni/Ava/Web.hs index f9dd7cd..c1eaa48 100644 --- a/Omni/Ava/Web.hs +++ b/Omni/Ava/Web.hs @@ -1,4 +1,6 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeOperators #-} {-# LANGUAGE NoImplicitPrelude #-} -- | Web server for Ava trace viewer. @@ -8,6 +10,9 @@ -- : out omni-ava-web -- : dep warp -- : dep wai +-- : dep servant-server +-- : dep servant-lucid +-- : dep lucid -- : dep http-types -- : dep aeson -- : dep text @@ -23,14 +28,13 @@ where import Alpha import qualified Data.Aeson as Aeson -import qualified Data.ByteString.Lazy as LBS import qualified Data.Text as Text -import qualified Data.Text.Encoding as Encoding import qualified Database.SQLite.Simple as SQL -import qualified Network.HTTP.Types as HTTP -import qualified Network.Wai as Wai +import qualified Lucid import qualified Network.Wai.Handler.Warp as Warp import qualified Omni.Ava.Trace as Trace +import Servant +import qualified Servant.HTML.Lucid as Lucid main :: IO () main = putText "Use Omni.Ava for the main entry point" @@ -44,126 +48,121 @@ startWebServer port dbPath = do SQL.withConnection dbPath Trace.initTraceDb Warp.run port (app dbPath) -app :: FilePath -> Wai.Application -app dbPath request respond = do - case Wai.pathInfo request of - ["trace", tid] -> serveTracePage dbPath tid request respond - ["api", "trace", tid] -> serveTraceJson dbPath tid request respond - ["health"] -> respond <| Wai.responseLBS HTTP.status200 [(HTTP.hContentType, "text/plain")] "ok" - _ -> respond <| Wai.responseLBS HTTP.status404 [(HTTP.hContentType, "text/plain")] "Not found" - -serveTracePage :: FilePath -> Text -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived -serveTracePage dbPath tid _req respond = do - SQL.withConnection dbPath <| \conn -> do - maybeRec <- Trace.getTrace conn tid - case maybeRec of - Nothing -> respond <| Wai.responseLBS HTTP.status404 [(HTTP.hContentType, "text/plain")] "Trace not found" - Just rec -> do - html <- renderTraceHtml rec - respond <| Wai.responseLBS HTTP.status200 [(HTTP.hContentType, "text/html; charset=utf-8")] (LBS.fromStrict <| Encoding.encodeUtf8 html) - -serveTraceJson :: FilePath -> Text -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived -serveTraceJson dbPath tid _req respond = do - SQL.withConnection dbPath <| \conn -> do - maybeRec <- Trace.getTrace conn tid - case maybeRec of - Nothing -> respond <| Wai.responseLBS HTTP.status404 [(HTTP.hContentType, "application/json")] "{\"error\":\"not found\"}" - Just rec -> do - let json = Aeson.encode rec - respond <| Wai.responseLBS HTTP.status200 [(HTTP.hContentType, "application/json")] json - -renderTraceHtml :: Trace.TraceRecord -> IO Text -renderTraceHtml rec = - pure - <| Text.replace "{{trace_id}}" (Trace.trcId rec) - <| Text.replace "{{tool_name}}" (Trace.trcToolName rec) - <| Text.replace "{{created_at}}" (Trace.trcCreatedAt rec) - <| Text.replace "{{duration_ms}}" (tshow <| Trace.trcDurationMs rec) - <| Text.replace "{{input_json}}" (escapeHtml <| Trace.trcInput rec) - <| Text.replace "{{output_json}}" (escapeHtml <| Trace.trcOutput rec) - <| traceTemplate - --- | Escape HTML special characters -escapeHtml :: Text -> Text -escapeHtml = - Text.replace "&" "&" - <. Text.replace "<" "<" - <. Text.replace ">" ">" - <. Text.replace "\"" """ - --- | Embedded trace HTML template -traceTemplate :: Text -traceTemplate = +type API = + "trace" :> Capture "id" Text :> Get '[Lucid.HTML] TracePage + :<|> "api" :> "trace" :> Capture "id" Text :> Get '[JSON] Aeson.Value + :<|> "health" :> Get '[PlainText] Text + +api :: Proxy API +api = Proxy + +app :: FilePath -> Application +app dbPath = serve api (server dbPath) + +server :: FilePath -> Server API +server dbPath = + tracePageHandler dbPath + :<|> traceJsonHandler dbPath + :<|> healthHandler + +healthHandler :: Servant.Handler Text +healthHandler = pure "ok" + +tracePageHandler :: FilePath -> Text -> Servant.Handler TracePage +tracePageHandler dbPath tid = do + maybeRec <- liftIO <| SQL.withConnection dbPath <| \conn -> Trace.getTrace conn tid + case maybeRec of + Nothing -> throwError err404 + Just rec -> pure (TracePage rec) + +traceJsonHandler :: FilePath -> Text -> Servant.Handler Aeson.Value +traceJsonHandler dbPath tid = do + maybeRec <- liftIO <| SQL.withConnection dbPath <| \conn -> Trace.getTrace conn tid + case maybeRec of + Nothing -> throwError err404 + Just rec -> pure (Aeson.toJSON rec) + +-- | Wrapper for trace page rendering +newtype TracePage = TracePage Trace.TraceRecord + +instance Lucid.ToHtml TracePage where + toHtmlRaw = Lucid.toHtml + toHtml (TracePage rec) = + Lucid.doctypehtml_ <| do + Lucid.head_ <| do + Lucid.meta_ [Lucid.charset_ "utf-8"] + Lucid.meta_ [Lucid.name_ "viewport", Lucid.content_ "width=device-width, initial-scale=1"] + Lucid.title_ <| Lucid.toHtml ("Trace: " <> Trace.trcToolName rec) + Lucid.style_ traceStyles + Lucid.body_ <| do + Lucid.h1_ <| Lucid.toHtml (Trace.trcToolName rec) + Lucid.p_ [Lucid.class_ "meta"] <| do + Lucid.toHtml (Trace.trcCreatedAt rec) + " · " + Lucid.toHtml (tshow (Trace.trcDurationMs rec) <> "ms") + + Lucid.div_ [Lucid.class_ "section", Lucid.id_ "input-section"] <| do + Lucid.div_ [Lucid.class_ "section-header"] <| do + "Input" + Lucid.button_ [Lucid.class_ "copy-btn", Lucid.data_ "target" "input-content"] "Copy" + Lucid.div_ [Lucid.class_ "section-content", Lucid.id_ "input-content"] + <| Lucid.pre_ + <| Lucid.toHtml (Trace.trcInput rec) + + Lucid.div_ [Lucid.class_ "section", Lucid.id_ "output-section"] <| do + Lucid.div_ [Lucid.class_ "section-header"] <| do + "Output" + Lucid.button_ [Lucid.class_ "copy-btn", Lucid.data_ "target" "output-content"] "Copy" + Lucid.div_ [Lucid.class_ "section-content", Lucid.id_ "output-content"] + <| Lucid.pre_ + <| Lucid.toHtml (Trace.trcOutput rec) + + Lucid.p_ [Lucid.class_ "footer"] <| Lucid.toHtml ("Trace ID: " <> Trace.trcId rec) + + Lucid.script_ traceScript + +traceStyles :: Text +traceStyles = Text.unlines - [ "", - "", - "", - " ", - " ", - " Trace: {{tool_name}}", - " ", - "", - "", - "

{{tool_name}}

", - "

{{created_at}} · {{duration_ms}}ms

", - "", - "
", - "
", - " Input", - " ", - "
", - "
{{input_json}}
", - "
", - "", - "
", - "
", - " Output", - " ", - "
", - "
{{output_json}}
", - "
", - "", - "

Trace ID: {{trace_id}}

", - "", - " ", - "", - "" + " });", + "});" ] -- cgit v1.2.3