feat: wire up cook log DB at server startup

This commit is contained in:
2026-05-25 22:00:45 -04:00
parent ceadf50c59
commit c6a9c2dee4
2 changed files with 196 additions and 12 deletions
+78 -7
View File
@@ -47,8 +47,13 @@ import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString as BS
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.Text.Encoding as TE
import qualified Database.SQLite.Simple as SQL
import Data.Time.Calendar (Day, fromGregorianValid)
import qualified Data.Time.Format as Time
import Text.Read (readMaybe)
import Roux.Config (AnthropicConfig (..), RouxConfig (..))
import qualified Roux.CooklangPrint as CooklangPrint
import qualified Roux.CookLog as CookLog
import qualified Roux.Html as Html
import qualified Roux.RecipeIndex as Idx
import Roux.SchemaOrg (SchemaOrgRecipe (..), parseSchemaOrgRecipe, schemaOrgToCooklang)
@@ -95,7 +100,11 @@ app recipeDir config = do
forkIO $
watchRecipes absDir state changeLog `catch` \e ->
putStrLn $ "[roux] watcher thread crashed: " <> show (e :: SomeException)
pure (router absDir config state changeLog)
-- Initialize cook log database
let dbPath = recipeDir </> "cook-log.db"
db <- CookLog.initDb dbPath
putStrLn $ "[roux] cook log DB: " <> dbPath
pure (router absDir config state changeLog db)
where
isRight (Right _) = True
isRight (Left _) = False
@@ -177,8 +186,8 @@ sseHandler changeLog _request respond = do
go `finally` pure ()
-- | Route requests — SSE endpoint, recipe pages, or import.
router :: FilePath -> RouxConfig -> IORef (Map FilePath Idx.RecipeInfo) -> ChangeLog -> Wai.Application
router recipeDir config state changeLog request respond =
router :: FilePath -> RouxConfig -> IORef (Map FilePath Idx.RecipeInfo) -> ChangeLog -> SQL.Connection -> Wai.Application
router recipeDir config state changeLog db request respond =
case Wai.pathInfo request of
["events"] -> sseHandler changeLog request respond
["import"] -> importHandler recipeDir config state request respond
@@ -187,7 +196,16 @@ router recipeDir config state changeLog request respond =
["recipes", _, "title"]
| Wai.requestMethod request == "PATCH" ->
titleUpdateHandler recipeDir state request respond
_ -> handleRequest state request respond
pathInfo
| Just filename <- dispatchLogPost pathInfo
, Wai.requestMethod request == "POST" ->
handleCookLogPost db filename request respond
_ -> handleRequest state db request respond
-- | Extract filename from /cook-log/{filename} POST path, if it matches.
dispatchLogPost :: [Text] -> Maybe Text
dispatchLogPost ["cook-log", filename] = Just filename
dispatchLogPost _ = Nothing
{- | Handle GET and POST requests to /import.
GET — show the import form.
@@ -604,8 +622,8 @@ reReadRecipe dir path state changeLog = do
putStrLn $ "[roux] updated " <> path
-- | Route an incoming request to the appropriate handler.
handleRequest :: IORef (Map FilePath Idx.RecipeInfo) -> Wai.Application
handleRequest state request respond = do
handleRequest :: IORef (Map FilePath Idx.RecipeInfo) -> SQL.Connection -> Wai.Application
handleRequest state db request respond = do
recipes <- readIORef state
let recipeList = Map.elems recipes
case Wai.pathInfo request of
@@ -618,10 +636,63 @@ handleRequest state request respond = do
-- Recipe detail page
["recipes", rawPath] ->
case lookupRecipe (urlDecodePath rawPath) recipes of
Just info -> respond (htmlResponse (Html.recipePage info))
Just info -> do
let filename = T.pack (map toLower (stripCookExt (urlDecodePath rawPath)))
entries <- CookLog.fetchEntries db filename
respond (htmlResponse (Html.recipePage info entries))
Nothing -> respond notFound
-- Cook log JSON endpoint for a specific recipe
["cook-log", filename] -> do
entries <- CookLog.fetchEntries db filename
let body = A.encode entries
respond $ Wai.responseLBS HTTP.status200
[("Content-Type", "application/json")]
body
-- Cook history page (all entries across recipes)
["cook-log"] -> do
entries <- CookLog.fetchAllEntries db
let grouped = groupEntries entries
respond $ htmlResponse (Html.cookHistoryPage grouped)
_ -> respond notFound
-- | Handle POST /cook-log/{filename}
handleCookLogPost :: SQL.Connection -> Text -> Wai.Application
handleCookLogPost db filename request respond = do
body <- readRequestBody request
let params = parseFormBody body
dateStr = fromMaybe "" (lookup "cooked-date" params)
comment = lookup "comment" params
case parseDate dateStr of
Just day -> do
_ <- CookLog.insertEntry db filename day comment
let resp = Wai.responseLBS HTTP.status200 [("Content-Type", "application/json")]
(LB.fromStrict (encodeUtf8 "{\"ok\":true}"))
respond resp
Nothing ->
let resp = Wai.responseLBS HTTP.status400
[("Content-Type", "application/json")]
(LB.fromStrict (encodeUtf8 "{\"error\":\"Invalid date format\"}"))
in respond resp
-- | Parse a YYYY-MM-DD date string.
parseDate :: Text -> Maybe Day
parseDate t = case T.splitOn "-" t of
[y, m, d] -> do
y' <- readMaybe (T.unpack y)
m' <- readMaybe (T.unpack m)
d' <- readMaybe (T.unpack d)
fromGregorianValid y' m' d'
_ -> Nothing
-- | Group entries by month label (e.g. "March 2026").
groupEntries :: [CookLog.CookEntry] -> [(Text, [CookLog.CookEntry])]
groupEntries [] = []
groupEntries entries =
let monthKey d = T.pack $ Time.formatTime Time.defaultTimeLocale "%B %Y" d
groups = Map.fromListWith (++) [(monthKey (CookLog.ceCookedDate e), [e]) | e <- entries]
sortedMonths = reverse (Map.keys groups)
in [(m, Map.findWithDefault [] m groups) | m <- sortedMonths]
-- | Look up a recipe by filename (case-insensitive, .cook extension optional).
lookupRecipe :: FilePath -> Map FilePath Idx.RecipeInfo -> Maybe Idx.RecipeInfo
lookupRecipe target =