From 0b6b9c3d17b827cf3fff8d0b300838ee7ede90bc Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Mon, 25 May 2026 22:10:23 -0400 Subject: [PATCH] fix: use Text for created_at, fix show-bug in ToJSON, add /cook-history route --- .gitignore | 1 + roux-server.cabal | 1 + src/Roux/CookLog.hs | 25 +++++++++++++------------ src/Roux/Server.hs | 8 +++++++- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 7da5b39..3f0da94 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ build/ __pycache__ .worktrees/ .superpowers/ +recipes/ diff --git a/roux-server.cabal b/roux-server.cabal index b6e9dbc..b256218 100644 --- a/roux-server.cabal +++ b/roux-server.cabal @@ -20,6 +20,7 @@ library Roux Roux.Config Roux.CooklangPrint + Roux.CookLog Roux.Html Roux.NYTimes Roux.Parser diff --git a/src/Roux/CookLog.hs b/src/Roux/CookLog.hs index 0c11ea8..9bab7e3 100644 --- a/src/Roux/CookLog.hs +++ b/src/Roux/CookLog.hs @@ -18,7 +18,7 @@ import qualified Data.Aeson as A import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar (Day, fromGregorianValid) -import Data.Time.Clock (UTCTime, getCurrentTime) +import Data.Time.Clock (getCurrentTime) import qualified Data.Time.Format as Time import Database.SQLite.Simple (Connection, FromRow (..), Only (..), Query) import qualified Database.SQLite.Simple as SQL @@ -29,12 +29,17 @@ data CookEntry = CookEntry , ceRecipeFilename :: !Text , ceCookedDate :: !Day , ceComment :: !Text - , ceCreatedAt :: !UTCTime + , ceCreatedAt :: !Text } deriving stock (Eq, Show) instance FromRow CookEntry where - fromRow = CookEntry <$> SQL.field <*> SQL.field <*> SQL.field <*> SQL.field <*> SQL.field + fromRow = CookEntry <$> SQL.field <*> SQL.field <*> SQL.field <*> SQL.field <*> (trimQuotes <$> SQL.field) + +-- | Strip leading/trailing double-quote characters from a Text value. +-- This handles any accidental quote wrapping from the DB round-trip. +trimQuotes :: Text -> Text +trimQuotes = T.dropAround (== '"') instance ToJSON CookEntry where toJSON e = A.object @@ -42,7 +47,7 @@ instance ToJSON CookEntry where , "recipe_filename" A..= A.toJSON (ceRecipeFilename e) , "cooked_date" A..= A.toJSON (show (ceCookedDate e)) , "comment" A..= A.toJSON (ceComment e) - , "created_at" A..= A.toJSON (show (ceCreatedAt e)) + , "created_at" A..= A.toJSON (ceCreatedAt e) ] instance FromJSON CookEntry where @@ -52,19 +57,14 @@ instance FromJSON CookEntry where <*> o A..: "recipe_filename" <*> (parseDate =<< o A..: "cooked_date") <*> o A..: "comment" - <*> (parseUTC =<< o A..: "created_at") - where + <*> o A..: "created_at" + where parseDate t = case T.splitOn "-" t of [y, m, d] -> case fromGregorianValid (read (T.unpack y)) (read (T.unpack m)) (read (T.unpack d)) of Just day -> pure day Nothing -> fail "invalid date" _ -> fail "expected YYYY-MM-DD" - parseUTC t = case T.unpack t of - "" -> fail "empty datetime" - s -> case Time.parseTimeM True Time.defaultTimeLocale "%Y-%m-%d %H:%M:%S" s of - Just ut -> pure ut - Nothing -> fail "invalid datetime" -- | Open or create the DB, ensuring the table exists. initDb :: FilePath -> IO Connection @@ -87,11 +87,12 @@ initDb dbPath = do insertEntry :: Connection -> Text -> Day -> Maybe Text -> IO Int insertEntry conn filename day mComment = do now <- getCurrentTime + let nowStr = T.pack $ Time.formatTime Time.defaultTimeLocale "%Y-%m-%d %H:%M:%S" now SQL.withTransaction conn $ do SQL.execute conn "INSERT INTO cook_log (recipe_filename, cooked_date, comment, created_at) VALUES (?, ?, ?, ?)" - (filename, show day, T.strip (fromMaybe "" mComment), show now) + (filename, T.pack (show day), T.strip (fromMaybe "" mComment), nowStr) rows <- SQL.query conn "SELECT last_insert_rowid()" () case rows of [Only i] -> pure i diff --git a/src/Roux/Server.hs b/src/Roux/Server.hs index e47f105..962dc19 100644 --- a/src/Roux/Server.hs +++ b/src/Roux/Server.hs @@ -649,10 +649,16 @@ handleRequest state db request respond = do [("Content-Type", "application/json")] body -- Cook history page (all entries across recipes) - ["cook-log"] -> do + ["cook-history"] -> do entries <- CookLog.fetchAllEntries db let grouped = groupEntries entries respond $ htmlResponse (Html.cookHistoryPage grouped) + -- Cook log JSON endpoint for all entries + ["cook-log"] -> do + entries <- CookLog.fetchAllEntries db + respond $ Wai.responseLBS HTTP.status200 + [("Content-Type", "application/json")] + (A.encode entries) _ -> respond notFound -- | Handle POST /cook-log/{filename}