From 5055e86eb4b07aba9ff3958759aaeb85571fcfef Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 4 Nov 2025 12:07:47 +0100 Subject: [PATCH 1/4] Add simple web API --- exe-t4-api/API.hs | 37 +++++++++++++++++++++++++++++++ exe-t4-api/static/index.html | 9 ++++++++ terminal-time-tracking-tool.cabal | 13 +++++++++++ 3 files changed, 59 insertions(+) create mode 100644 exe-t4-api/API.hs create mode 100644 exe-t4-api/static/index.html diff --git a/exe-t4-api/API.hs b/exe-t4-api/API.hs new file mode 100644 index 0000000..81f6187 --- /dev/null +++ b/exe-t4-api/API.hs @@ -0,0 +1,37 @@ +import T4.Data +import T4.Storage +import qualified Data.Set as S +import Control.Monad.IO.Class +import Servant +import Network.Wai.Handler.Warp +import qualified Paths_terminal_time_tracking_tool as Paths + +type API = "api" :> T4API :<|> Raw + +type T4API = Get '[JSON] (Maybe Clock) -- status + :<|> "clocks" :> Get '[JSON] [Clock] + :<|> "categories" :> Get '[JSON] [Category] + :<|> "tags" :> Get '[JSON] [Tag] + +server :: FilePath -> Server API +server staticDir = apiServer :<|> serveDirectoryFileServer staticDir + +apiServer :: Server T4API +apiServer = getStatus + :<|> getClocks + :<|> getCategories + :<|> getTags + + where getStatus = findMax <$> clocks + getClocks = S.toList <$> clocks + getCategories = S.toList . allCategories <$> clocks + getTags = S.toList . allTags <$> clocks + clocks = do dir <- liftIO getStorageDirectory + liftIO $ loadDataFromDir dir + findMax = fmap fst . S.maxView + +main :: IO () +main = do + staticDir <- Paths.getDataFileName "exe-t4-api/static" + putStrLn "Listening on http://localhost:8080..." + run 8080 $ serve (Proxy :: Proxy API) $ server staticDir diff --git a/exe-t4-api/static/index.html b/exe-t4-api/static/index.html new file mode 100644 index 0000000..c7727f1 --- /dev/null +++ b/exe-t4-api/static/index.html @@ -0,0 +1,9 @@ + + + + Hello! + + +

Hello!

+ + diff --git a/terminal-time-tracking-tool.cabal b/terminal-time-tracking-tool.cabal index eefc8f3..4a8452d 100644 --- a/terminal-time-tracking-tool.cabal +++ b/terminal-time-tracking-tool.cabal @@ -9,6 +9,7 @@ build-type: Simple synopsis: t4: terminal time tracking tool homepage: https://github.com/memowe/t4 bug-reports: https://github.com/memowe/t4/issues +data-files: exe-t4-api/static/**/* source-repository head type: git @@ -56,6 +57,18 @@ executable t5 hs-source-dirs: exe-t5-interactive main-is: TUI.hs +executable t4-api + import: basics + default-extensions: DataKinds + , TypeOperators + build-depends: terminal-time-tracking-tool + , servant-server + , warp + hs-source-dirs: exe-t4-api + main-is: API.hs + autogen-modules: Paths_terminal_time_tracking_tool + other-modules: Paths_terminal_time_tracking_tool + test-suite library-test import: basics ghc-options: -Wno-orphans From 4dca4bf8ed9f1f0966cc1d513d6d6d0b733c3f70 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 4 Nov 2025 14:41:37 +0100 Subject: [PATCH 2/4] Add openAPI 3.0 and swagger UI to web API --- .github/workflows/haskell-ci.yml | 1 + exe-t4-api/API.hs | 43 ++++++++++++++++++++++++++----- terminal-time-tracking-tool.cabal | 8 +++++- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 4ef3d1e..7561002 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -5,6 +5,7 @@ jobs: build_test_doc: runs-on: ubuntu-latest steps: + - run: sudo apt install -y liblzma-dev pkg-config - uses: actions/checkout@v4 - uses: haskell-actions/setup@v2 id: setup diff --git a/exe-t4-api/API.hs b/exe-t4-api/API.hs index 81f6187..d3f0814 100644 --- a/exe-t4-api/API.hs +++ b/exe-t4-api/API.hs @@ -2,19 +2,44 @@ import T4.Data import T4.Storage import qualified Data.Set as S import Control.Monad.IO.Class +import Control.Lens +import Data.Aeson import Servant +import Data.OpenApi hiding (Tag, Server, server) +import Servant.OpenApi +import Servant.Swagger.UI import Network.Wai.Handler.Warp import qualified Paths_terminal_time_tracking_tool as Paths -type API = "api" :> T4API :<|> Raw +instance ToSchema Clock where + declareNamedSchema _ = do + return $ NamedSchema (Just "Clock") $ mempty + & type_ ?~ OpenApiObject + & example ?~ toJSON clock + where clock = In (SLT (read "2025-11-04 09:15:00")) + (Just "T4 programming") + (S.fromList ["backend", "api", "haskell"]) -type T4API = Get '[JSON] (Maybe Clock) -- status - :<|> "clocks" :> Get '[JSON] [Clock] - :<|> "categories" :> Get '[JSON] [Category] - :<|> "tags" :> Get '[JSON] [Tag] +t4OpenApi :: OpenApi +t4OpenApi = toOpenApi (Proxy :: Proxy T4API) + & info . title .~ "T4: Terminal Time Tracking Tool API" + & info . description ?~ "Read-only API for T4 time tracking" + +type API = "api" :> T4API + :<|> "openapi.json" :> Get '[JSON] OpenApi + :<|> SwaggerSchemaUI "swagger-ui" "swagger.json" + :<|> Raw + +type T4API = Get '[JSON] (Maybe Clock) + :<|> "clocks" :> Get '[JSON] [Clock] + :<|> "categories" :> Get '[JSON] [Category] + :<|> "tags" :> Get '[JSON] [Tag] server :: FilePath -> Server API -server staticDir = apiServer :<|> serveDirectoryFileServer staticDir +server staticDir = apiServer + :<|> pure t4OpenApi + :<|> swaggerSchemaUIServer t4OpenApi + :<|> serveDirectoryFileServer staticDir apiServer :: Server T4API apiServer = getStatus @@ -33,5 +58,9 @@ apiServer = getStatus main :: IO () main = do staticDir <- Paths.getDataFileName "exe-t4-api/static" - putStrLn "Listening on http://localhost:8080..." + putStrLn "T4 API Server starting..." + putStrLn " Listening on: http://localhost:8080" + putStrLn " OpenAPI spec at: http://localhost:8080/openapi.json" + putStrLn " Swagger UI at: http://localhost:8080/swagger-ui" + putStrLn " API endpoints: http://localhost:8080/api/*" run 8080 $ serve (Proxy :: Proxy API) $ server staticDir diff --git a/terminal-time-tracking-tool.cabal b/terminal-time-tracking-tool.cabal index 4a8452d..5bfca79 100644 --- a/terminal-time-tracking-tool.cabal +++ b/terminal-time-tracking-tool.cabal @@ -61,13 +61,19 @@ executable t4-api import: basics default-extensions: DataKinds , TypeOperators + , FlexibleInstances + ghc-options: -Wno-orphans build-depends: terminal-time-tracking-tool , servant-server + , servant-openapi3 + , openapi3 + , servant-swagger-ui , warp + , lens hs-source-dirs: exe-t4-api main-is: API.hs - autogen-modules: Paths_terminal_time_tracking_tool other-modules: Paths_terminal_time_tracking_tool + autogen-modules: Paths_terminal_time_tracking_tool test-suite library-test import: basics From ea6644cbeb4020bfefa3be6b7129331dbc15c22c Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 4 Nov 2025 17:37:57 +0100 Subject: [PATCH 3/4] Remove API URL prefix (to fix swagger UI) --- exe-t4-api/API.hs | 5 ++--- exe-t4-api/static/{index.html => report.html} | 0 2 files changed, 2 insertions(+), 3 deletions(-) rename exe-t4-api/static/{index.html => report.html} (100%) diff --git a/exe-t4-api/API.hs b/exe-t4-api/API.hs index d3f0814..8ed5b62 100644 --- a/exe-t4-api/API.hs +++ b/exe-t4-api/API.hs @@ -25,7 +25,7 @@ t4OpenApi = toOpenApi (Proxy :: Proxy T4API) & info . title .~ "T4: Terminal Time Tracking Tool API" & info . description ?~ "Read-only API for T4 time tracking" -type API = "api" :> T4API +type API = T4API :<|> "openapi.json" :> Get '[JSON] OpenApi :<|> SwaggerSchemaUI "swagger-ui" "swagger.json" :<|> Raw @@ -59,8 +59,7 @@ main :: IO () main = do staticDir <- Paths.getDataFileName "exe-t4-api/static" putStrLn "T4 API Server starting..." - putStrLn " Listening on: http://localhost:8080" putStrLn " OpenAPI spec at: http://localhost:8080/openapi.json" putStrLn " Swagger UI at: http://localhost:8080/swagger-ui" - putStrLn " API endpoints: http://localhost:8080/api/*" + putStrLn " API endpoints: http://localhost:8080/*" run 8080 $ serve (Proxy :: Proxy API) $ server staticDir diff --git a/exe-t4-api/static/index.html b/exe-t4-api/static/report.html similarity index 100% rename from exe-t4-api/static/index.html rename to exe-t4-api/static/report.html From 44b7df12d34e53aa5c2344d8cf7a9aa90d6933d0 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 4 Nov 2025 18:01:14 +0100 Subject: [PATCH 4/4] Make report placeholdership explicit --- exe-t4-api/static/report.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exe-t4-api/static/report.html b/exe-t4-api/static/report.html index c7727f1..0c7f40c 100644 --- a/exe-t4-api/static/report.html +++ b/exe-t4-api/static/report.html @@ -1,9 +1,9 @@ - Hello! + t4 Report -

Hello!

+

t4 report placeholder