htmx flags

This commit is contained in:
MasseR committed 2026-10-12 14:55:30 +03:00
1 parent 87dda96e7e
commit 45277e02da
1 file changed
+51 -13
+51 -13
View File
@@ -17,20 +17,22 @@ import Data.Maybe (fromMaybe)
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Set as S import qualified Data.Set as S
import Data.Text (Text) import Data.Text (Text)
import HomeAssistant.Runtime.Flags (Flags, allFlags, setEnabled, validNames) import HomeAssistant.Runtime.Flags (Flags, allFlags, setEnabled, validNames, isEnabled)
import Control.Exception.Annotated (checkpoint, Annotation (Annotation)) import Control.Exception.Annotated (checkpoint, Annotation (Annotation))
import Servant.Server.Generic (genericServeT) import Servant.Server.Generic (genericServeT)
import Network.Wai (Application) import Network.Wai (Application)
import Network.HTTP.Media ((//)) import Network.HTTP.Media ((//))
import HomeAssistant.Runtime.Graphing (Graph (..), Range (..), lookupGraph, graphs, renderGraph) import HomeAssistant.Runtime.Graphing (Graph (..), Range (..), lookupGraph, graphs, renderGraph)
import Katip (KatipContextT, Katip, runKatipContextT, LogEnv, KatipContext, ls, Severity (..), logFM) import Katip (KatipContextT, Katip, runKatipContextT, LogEnv, KatipContext, ls, Severity (..), logFM, katipAddContext, sl)
import Data.Aeson (ToJSON, FromJSON) import Data.Aeson (ToJSON (toJSON), FromJSON, object, (.=))
import Lucid (ToHtml, toHtml, renderBS, toHtmlRaw, HtmlT) import Lucid (ToHtml, toHtml, renderBS, toHtmlRaw, HtmlT, Attributes)
import qualified Lucid.Html5 as H5 import qualified Lucid.Html5 as H5
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Foldable (for_) import Data.Foldable (for_)
import qualified Data.Text as T import qualified Data.Text as T
import Web.FormUrlEncoded (FromForm, fromForm, parseUnique) import Web.FormUrlEncoded (FromForm, fromForm, parseUnique)
import Lucid.Base (makeAttributes)
import Data.Aeson.Key (fromText)
data ImagePng data ImagePng
@@ -56,12 +58,13 @@ instance ToHtml StatsPage where
toHtml (StatsPage inner) = H5.doctypehtml_ $ H5.body_ inner toHtml (StatsPage inner) = H5.doctypehtml_ $ H5.body_ inner
toHtmlRaw = toHtml toHtmlRaw = toHtml
data API mode = API data API mode = API
{ getMetrics :: mode :- "metrics" :> Capture "graph" Graph :> QueryParam "range" Range :> Get '[ImagePng] GraphPng { getMetrics :: mode :- "metrics" :> Capture "graph" Graph :> QueryParam "range" Range :> Get '[ImagePng] GraphPng
, getStats :: mode :- "stats" :> QueryParam "range" Range :> Get '[HTML] StatsPage , getStats :: mode :- "stats" :> QueryParam "range" Range :> Get '[HTML] StatsPage
, getFlags :: mode :- "flags" :> Get '[JSON, HTML] FlagsResponse , getFlags :: mode :- "flags" :> Get '[JSON, HTML] FlagsResponse
, putFlag :: mode :- "flags" :> Capture "name" Text :> ReqBody '[JSON] FlagValue :> Put '[JSON] FlagsResponse , putFlag :: mode :- "flags" :> Capture "name" Text :> ReqBody '[JSON, FormUrlEncoded] FlagValue :> Put '[HTML, JSON] FlagResponse
, postFlag :: mode :- "flags" :> Capture "name" Text :> ReqBody '[JSON, FormUrlEncoded] FlagValue :> Post '[JSON, HTML] FlagsResponse , postFlag :: mode :- "flags" :> Capture "name" Text :> ReqBody '[JSON, FormUrlEncoded] FlagValue :> Post '[JSON, HTML] FlagResponse
} }
deriving Generic deriving Generic
@@ -76,17 +79,52 @@ instance FromForm FlagValue where
newtype FlagsResponse = FlagsResponse (Map Text Bool) newtype FlagsResponse = FlagsResponse (Map Text Bool)
deriving (ToJSON) via (Map Text Bool) deriving (ToJSON) via (Map Text Bool)
data FlagResponse = FlagResponse Text Bool
instance ToHtml FlagResponse where
toHtml (FlagResponse name value) = flagRow name value
toHtmlRaw = toHtml
instance ToJSON FlagResponse where
toJSON (FlagResponse k v) = object [fromText k .= v]
flagsPage :: Monad m => FlagsResponse -> HtmlT m () flagsPage :: Monad m => FlagsResponse -> HtmlT m ()
flagsPage (FlagsResponse f) = H5.doctypehtml_ $ flagsPage (FlagsResponse f) = H5.doctypehtml_ $ do
H5.body_ $ H5.table_ $ for_ (M.toList f) $ \(k,v) -> H5.tr_ $ -- <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.11/dist/htmx.min.js"
H5.td_ (toHtml k) <> H5.td_ (toHtml $ show v) <> H5.td_ (toggle k v) -- integrity="sha384-2OatzQy1H+Zd/IIrjr1TcuDGqLXeHhbooAyJY1KdQMKnr4LZ22k31GBLdYKHmVjg"
-- crossorigin="anonymous"></script>
H5.head_ $ do
H5.script_
[ H5.src_ "https://cdn.jsdelivr.net/npm/htmx.org@2.0.11/dist/htmx.min.js"
, H5.integrity_ "sha384-2OatzQy1H+Zd/IIrjr1TcuDGqLXeHhbooAyJY1KdQMKnr4LZ22k31GBLdYKHmVjg"
, H5.crossorigin_ "anonymous"
]
("" :: Text)
H5.body_ $ H5.table_ $ for_ (M.toList f) $ \(k,v) -> H5.tr_ [H5.id_ ("status-" <> k)] $ flagRow k v
flagRow :: Monad m => Text -> Bool -> HtmlT m ()
flagRow name value =
H5.td_ (toHtml name) <> H5.td_ (toHtml $ show value) <> H5.td_ (toggle name value)
where where
target k = "status-" <> k
toggle :: Monad m => Text -> Bool -> HtmlT m () toggle :: Monad m => Text -> Bool -> HtmlT m ()
toggle key current = H5.form_ [H5.method_ "post", H5.action_ $ postFlagsLink key] $ do toggle key current = H5.form_ [H5.method_ "post", H5.action_ $ postFlagsLink key] $ do
H5.input_ [H5.name_ "flag-value", H5.type_ "hidden", H5.value_ (T.pack $ show (not current))] H5.input_ [H5.name_ "flag-value", H5.type_ "hidden", H5.value_ (T.pack $ show (not current))]
H5.input_ [H5.type_ "submit", H5.value_ "Toggle"] H5.input_ [H5.type_ "submit", H5.value_ "Toggle", hxTrigger "click", hxTarget ("#" <> target key), hxPut (postFlagsLink key), hxSwap "outerHtml"]
postFlagsLink k = "/" <> toUrlPiece (fieldLink putFlag k) postFlagsLink k = "/" <> toUrlPiece (fieldLink putFlag k)
hxTrigger :: Text -> Attributes
hxTrigger = makeAttributes "hx-trigger"
hxTarget :: Text -> Attributes
hxTarget = makeAttributes "hx-target"
hxPut :: Text -> Attributes
hxPut = makeAttributes "hx-put"
hxSwap :: Text -> Attributes
hxSwap = makeAttributes "hx-swap"
instance ToHtml FlagsResponse where instance ToHtml FlagsResponse where
toHtml = flagsPage toHtml = flagsPage
toHtmlRaw = toHtml toHtmlRaw = toHtml
@@ -94,14 +132,14 @@ instance ToHtml FlagsResponse where
getFlagsHandler :: Flags -> LoggingHandler FlagsResponse getFlagsHandler :: Flags -> LoggingHandler FlagsResponse
getFlagsHandler = fmap FlagsResponse . allFlags getFlagsHandler = fmap FlagsResponse . allFlags
putFlagHandler :: Flags -> Text -> FlagValue -> LoggingHandler FlagsResponse putFlagHandler :: Flags -> Text -> FlagValue -> LoggingHandler FlagResponse
putFlagHandler flags name (FlagValue val) putFlagHandler flags name (FlagValue val)
| name `S.notMember` validNames flags = throwM err404 | name `S.notMember` validNames flags = throwM err404
| otherwise = do | otherwise = checkpoint (Annotation (name, val)) $ katipAddContext (sl "flag" (name, val)) $ do
setEnabled flags name val `catch` \e -> do setEnabled flags name val `catch` \e -> do
logFM ErrorS (ls ("failed to persist handler flag: " <> show (e :: SomeException))) logFM ErrorS (ls ("failed to persist handler flag: " <> show (e :: SomeException)))
throwM err500 throwM err500
FlagsResponse <$> allFlags flags FlagResponse name <$> isEnabled flags name
getMetricsHandler :: FilePath -> FilePath -> Graph -> Maybe Range -> LoggingHandler GraphPng getMetricsHandler :: FilePath -> FilePath -> Graph -> Maybe Range -> LoggingHandler GraphPng