feat: wire up cook log DB at server startup
This commit is contained in:
+78
-7
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user