feat: minimal IHP scaffold — T01-T05, T08 of IRP-WP-0001

- flake.nix adapted from inter-hub: appName=ihp-railiance-probe, stripped to
  core packages, GHC 9.10.3 Bug 1+2 overlays carried verbatim (pname check
  updated to ihp-railiance-probe-models)
- IHP project scaffold: Main.hs, Config.hs, App.cabal, Setup.hs, Makefile
- Schema: probes table (id, name, created_at)
- Health endpoint: GET /healthz → "ok" (HealthController)
- Probes CRUD: ProbesController + 4 views (Index, New, Show, Edit)
- Hspec test suite: Test/ProbeControllerSpec covers /probes and /healthz
- Helm chart in chart/: deployment, service, ingress, secret templates
- devenv.nix, devenv.yaml, .ghci, tailwind config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 12:57:28 +02:00
parent b818866c7f
commit e372a0c9ce
35 changed files with 645 additions and 0 deletions

7
Web/Controller/Health.hs Normal file
View File

@@ -0,0 +1,7 @@
module Web.Controller.Health where
import Web.Controller.Prelude
import Web.Types
instance Controller HealthController where
action HealthAction = renderPlain "ok"

12
Web/Controller/Prelude.hs Normal file
View File

@@ -0,0 +1,12 @@
module Web.Controller.Prelude
( module Web.Types
, module Generated.Types
, module IHP.Prelude
, module IHP.ControllerPrelude
) where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Web.Routes ()

42
Web/Controller/Probes.hs Normal file
View File

@@ -0,0 +1,42 @@
module Web.Controller.Probes where
import Web.Controller.Prelude
import Web.View.Probes.Index
import Web.View.Probes.New
import Web.View.Probes.Show
import Web.View.Probes.Edit
instance Controller ProbesController where
action ProbesAction = do
probes <- query @Probe |> fetch
render IndexView { .. }
action NewProbeAction = do
let probe = newRecord @Probe
render NewView { .. }
action ShowProbeAction { probeId } = do
probe <- fetch probeId
render ShowView { .. }
action CreateProbeAction = do
newRecord @Probe
|> fill @'["name"]
|> createRecord
redirectTo ProbesAction
action EditProbeAction { probeId } = do
probe <- fetch probeId
render EditView { .. }
action UpdateProbeAction { probeId } = do
probe <- fetch probeId
probe
|> fill @'["name"]
|> updateRecord
redirectTo ProbesAction
action DeleteProbeAction { probeId } = do
probe <- fetch probeId
deleteRecord probe
redirectTo ProbesAction

36
Web/FrontController.hs Normal file
View File

@@ -0,0 +1,36 @@
module Web.FrontController where
import IHP.RouterPrelude
import IHP.ControllerPrelude
import IHP.ViewPrelude (Html, hsx, Layout)
import Generated.Types
import Web.Types
import Web.Routes ()
import Web.Controller.Health ()
import Web.Controller.Probes ()
instance FrontController WebApplication where
controllers =
[ parseRoute @HealthController
, parseRoute @ProbesController
]
instance InitControllerContext WebApplication where
initContext = do
setLayout defaultLayout
defaultLayout :: (?context :: ControllerContext, ?request :: Request) => Layout
defaultLayout inner = [hsx|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ihp-railiance-probe</title>
</head>
<body>
{inner}
</body>
</html>
|]

16
Web/Routes.hs Normal file
View File

@@ -0,0 +1,16 @@
module Web.Routes where
import IHP.RouterPrelude
import Generated.Types
import Web.Types
instance CanRoute HealthController where
parseRoute' = do
pathPrefix "/healthz"
endOfInput
pure HealthAction
instance HasPath HealthController where
pathTo HealthAction = "/healthz"
instance AutoRoute ProbesController

21
Web/Types.hs Normal file
View File

@@ -0,0 +1,21 @@
module Web.Types where
import IHP.Prelude
import IHP.ModelSupport
import Generated.Types
data WebApplication = WebApplication deriving (Eq, Show)
data HealthController
= HealthAction
deriving (Eq, Show, Data)
data ProbesController
= ProbesAction
| NewProbeAction
| ShowProbeAction { probeId :: !(Id Probe) }
| CreateProbeAction
| EditProbeAction { probeId :: !(Id Probe) }
| UpdateProbeAction { probeId :: !(Id Probe) }
| DeleteProbeAction { probeId :: !(Id Probe) }
deriving (Eq, Show, Data)

12
Web/View/Prelude.hs Normal file
View File

@@ -0,0 +1,12 @@
module Web.View.Prelude
( module Web.Types
, module Generated.Types
, module IHP.Prelude
, module IHP.ViewPrelude
) where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()

17
Web/View/Probes/Edit.hs Normal file
View File

@@ -0,0 +1,17 @@
module Web.View.Probes.Edit where
import Web.View.Prelude
data EditView = EditView { probe :: Probe }
instance View EditView where
html EditView { .. } = [hsx|
<h1>Edit Probe</h1>
{renderForm probe}
|]
renderForm :: Probe -> Html
renderForm probe = formFor probe [hsx|
{textField #name}
{submitButton}
|]

22
Web/View/Probes/Index.hs Normal file
View File

@@ -0,0 +1,22 @@
module Web.View.Probes.Index where
import Web.View.Prelude
data IndexView = IndexView { probes :: [Probe] }
instance View IndexView where
html IndexView { .. } = [hsx|
<h1>Probes</h1>
<a href={NewProbeAction}>New Probe</a>
<ul>
{forEach probes renderProbe}
</ul>
|]
renderProbe :: Probe -> Html
renderProbe probe = [hsx|
<li>
<a href={ShowProbeAction probe.id}>{probe.name}</a>
<a href={DeleteProbeAction probe.id} class="js-delete">Delete</a>
</li>
|]

17
Web/View/Probes/New.hs Normal file
View File

@@ -0,0 +1,17 @@
module Web.View.Probes.New where
import Web.View.Prelude
data NewView = NewView { probe :: Probe }
instance View NewView where
html NewView { .. } = [hsx|
<h1>New Probe</h1>
{renderForm probe}
|]
renderForm :: Probe -> Html
renderForm probe = formFor probe [hsx|
{textField #name}
{submitButton}
|]

11
Web/View/Probes/Show.hs Normal file
View File

@@ -0,0 +1,11 @@
module Web.View.Probes.Show where
import Web.View.Prelude
data ShowView = ShowView { probe :: Probe }
instance View ShowView where
html ShowView { .. } = [hsx|
<h1>{probe.name}</h1>
<a href={ProbesAction}>Back</a>
|]