diff --git a/.ghci b/.ghci
new file mode 100644
index 0000000..5ed7a92
--- /dev/null
+++ b/.ghci
@@ -0,0 +1,7 @@
+:set -XNoImplicitPrelude
+:def loadFromIHP \file -> (System.Environment.getEnv "IHP_LIB") >>= (\ihpLib -> readFile (ihpLib <> "/" <> file))
+:loadFromIHP applicationGhciConfig
+:set -j1
+:set -fkeep-going
+:set -fwrite-interface
+import IHP.Prelude
diff --git a/App.cabal b/App.cabal
new file mode 100644
index 0000000..152e688
--- /dev/null
+++ b/App.cabal
@@ -0,0 +1,57 @@
+-- Stub cabal file — only for HLS/tooling support. Not used by the build.
+-- See flake.nix for actual package management.
+name: App
+version: 0.1.0.0
+license: AllRightsReserved
+license-file: LICENSE
+author: Developers
+maintainer: developers@example.com
+build-type: Simple
+cabal-version: >=1.10
+
+executable App
+ main-is: Main.hs
+ build-depends:
+ ihp,
+ base,
+ wai,
+ text,
+ hspec,
+ ihp-hspec
+ hs-source-dirs: .
+ default-language: Haskell2010
+ extensions:
+ OverloadedStrings
+ , NoImplicitPrelude
+ , ImplicitParams
+ , Rank2Types
+ , DisambiguateRecordFields
+ , NamedFieldPuns
+ , DuplicateRecordFields
+ , OverloadedLabels
+ , FlexibleContexts
+ , TypeSynonymInstances
+ , FlexibleInstances
+ , QuasiQuotes
+ , TypeFamilies
+ , PackageImports
+ , ScopedTypeVariables
+ , RecordWildCards
+ , TypeApplications
+ , DataKinds
+ , InstanceSigs
+ , DeriveGeneric
+ , MultiParamTypeClasses
+ , TypeOperators
+ , DeriveDataTypeable
+ , MultiWayIf
+ , UndecidableInstances
+ , BlockArguments
+ , PartialTypeSignatures
+ , LambdaCase
+ , DefaultSignatures
+ , EmptyDataDeriving
+ , BangPatterns
+ , FunctionalDependencies
+ , StandaloneDeriving
+ , DerivingVia
diff --git a/Application/Fixtures.sql b/Application/Fixtures.sql
new file mode 100644
index 0000000..7fb5689
--- /dev/null
+++ b/Application/Fixtures.sql
@@ -0,0 +1 @@
+-- No fixtures for probe.
diff --git a/Application/Schema.sql b/Application/Schema.sql
new file mode 100644
index 0000000..561d8e4
--- /dev/null
+++ b/Application/Schema.sql
@@ -0,0 +1,7 @@
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+CREATE TABLE probes (
+ id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+ name TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
+);
diff --git a/Config/Config.hs b/Config/Config.hs
new file mode 100644
index 0000000..0366ac0
--- /dev/null
+++ b/Config/Config.hs
@@ -0,0 +1,9 @@
+module Config where
+
+import IHP.Prelude
+import IHP.Environment
+import IHP.FrameworkConfig
+
+config :: ConfigBuilder
+config = do
+ pure ()
diff --git a/Main.hs b/Main.hs
new file mode 100644
index 0000000..5a5f082
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,21 @@
+module Main where
+import IHP.Prelude
+
+import Config
+import qualified IHP.Server
+import IHP.RouterSupport
+import IHP.FrameworkConfig
+import IHP.Job.Types
+import Web.FrontController
+import Web.Types
+
+instance FrontController RootApplication where
+ controllers =
+ [ mountFrontController WebApplication
+ ]
+
+instance Worker RootApplication where
+ workers _ = []
+
+main :: IO ()
+main = IHP.Server.run config
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e4f35f3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+CSS_FILES += ${IHP}/static/vendor/bootstrap.min.css
+CSS_FILES += ${IHP}/static/vendor/flatpickr.min.css
+CSS_FILES += static/app.css
+
+JS_FILES += ${IHP}/static/vendor/jquery-3.6.0.slim.min.js
+JS_FILES += ${IHP}/static/vendor/timeago.js
+JS_FILES += ${IHP}/static/vendor/popper.min.js
+JS_FILES += ${IHP}/static/vendor/bootstrap.min.js
+JS_FILES += ${IHP}/static/vendor/flatpickr.js
+JS_FILES += ${IHP}/static/helpers.js
+JS_FILES += ${IHP}/static/vendor/morphdom-umd.min.js
+JS_FILES += ${IHP}/static/vendor/turbolinks.js
+JS_FILES += ${IHP}/static/vendor/turbolinksInstantClick.js
+JS_FILES += ${IHP}/static/vendor/turbolinksMorphdom.js
+
+include ${IHP}/Makefile.dist
diff --git a/PIPELINE_LOG.md b/PIPELINE_LOG.md
new file mode 100644
index 0000000..83d953f
--- /dev/null
+++ b/PIPELINE_LOG.md
@@ -0,0 +1,4 @@
+# Pipeline Validation Log
+
+| Date | SHA | Build | Push | Deploy | Smoke |
+|------|-----|-------|------|--------|-------|
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
index 0000000..9a994af
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/Test/Main.hs b/Test/Main.hs
new file mode 100644
index 0000000..ced3c97
--- /dev/null
+++ b/Test/Main.hs
@@ -0,0 +1,13 @@
+module Main where
+
+import Test.Hspec
+import IHP.Prelude
+import qualified Test.ProbeControllerSpec as ProbeController
+
+main :: IO ()
+main = hspec do
+ describe "Sanity" do
+ it "1 + 1 = 2" do
+ 1 + 1 `shouldBe` (2 :: Int)
+
+ ProbeController.spec
diff --git a/Test/ProbeControllerSpec.hs b/Test/ProbeControllerSpec.hs
new file mode 100644
index 0000000..6a3ce05
--- /dev/null
+++ b/Test/ProbeControllerSpec.hs
@@ -0,0 +1,14 @@
+module Test.ProbeControllerSpec where
+
+import Test.Hspec
+import IHP.HSpec
+
+spec :: Spec
+spec = describe "ProbeController" do
+ it "GET /probes returns 200" do
+ response <- get "/probes"
+ response `shouldRespondWith` 200
+
+ it "GET /healthz returns 200" do
+ response <- get "/healthz"
+ response `shouldRespondWith` 200
diff --git a/Web/Controller/Health.hs b/Web/Controller/Health.hs
new file mode 100644
index 0000000..ac6c83c
--- /dev/null
+++ b/Web/Controller/Health.hs
@@ -0,0 +1,7 @@
+module Web.Controller.Health where
+
+import Web.Controller.Prelude
+import Web.Types
+
+instance Controller HealthController where
+ action HealthAction = renderPlain "ok"
diff --git a/Web/Controller/Prelude.hs b/Web/Controller/Prelude.hs
new file mode 100644
index 0000000..152730d
--- /dev/null
+++ b/Web/Controller/Prelude.hs
@@ -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 ()
diff --git a/Web/Controller/Probes.hs b/Web/Controller/Probes.hs
new file mode 100644
index 0000000..5438834
--- /dev/null
+++ b/Web/Controller/Probes.hs
@@ -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
diff --git a/Web/FrontController.hs b/Web/FrontController.hs
new file mode 100644
index 0000000..5d25110
--- /dev/null
+++ b/Web/FrontController.hs
@@ -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|
+
+
+
+
+
+ ihp-railiance-probe
+
+
+ {inner}
+
+
+|]
diff --git a/Web/Routes.hs b/Web/Routes.hs
new file mode 100644
index 0000000..294e6aa
--- /dev/null
+++ b/Web/Routes.hs
@@ -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
diff --git a/Web/Types.hs b/Web/Types.hs
new file mode 100644
index 0000000..ee2189a
--- /dev/null
+++ b/Web/Types.hs
@@ -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)
diff --git a/Web/View/Prelude.hs b/Web/View/Prelude.hs
new file mode 100644
index 0000000..2640e7e
--- /dev/null
+++ b/Web/View/Prelude.hs
@@ -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 ()
diff --git a/Web/View/Probes/Edit.hs b/Web/View/Probes/Edit.hs
new file mode 100644
index 0000000..dde6c7a
--- /dev/null
+++ b/Web/View/Probes/Edit.hs
@@ -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|
+ Edit Probe
+ {renderForm probe}
+ |]
+
+renderForm :: Probe -> Html
+renderForm probe = formFor probe [hsx|
+ {textField #name}
+ {submitButton}
+|]
diff --git a/Web/View/Probes/Index.hs b/Web/View/Probes/Index.hs
new file mode 100644
index 0000000..1ad2cb1
--- /dev/null
+++ b/Web/View/Probes/Index.hs
@@ -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|
+ Probes
+ New Probe
+
+ {forEach probes renderProbe}
+
+ |]
+
+renderProbe :: Probe -> Html
+renderProbe probe = [hsx|
+
+ {probe.name}
+ Delete
+
+|]
diff --git a/Web/View/Probes/New.hs b/Web/View/Probes/New.hs
new file mode 100644
index 0000000..72b5882
--- /dev/null
+++ b/Web/View/Probes/New.hs
@@ -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|
+ New Probe
+ {renderForm probe}
+ |]
+
+renderForm :: Probe -> Html
+renderForm probe = formFor probe [hsx|
+ {textField #name}
+ {submitButton}
+|]
diff --git a/Web/View/Probes/Show.hs b/Web/View/Probes/Show.hs
new file mode 100644
index 0000000..7b63f1d
--- /dev/null
+++ b/Web/View/Probes/Show.hs
@@ -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|
+ {probe.name}
+ Back
+ |]
diff --git a/chart/Chart.yaml b/chart/Chart.yaml
new file mode 100644
index 0000000..8eeba0d
--- /dev/null
+++ b/chart/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: ihp-railiance-probe
+description: Minimal IHP probe app for Railiance01 pipeline validation
+type: application
+version: 0.1.0
+appVersion: "0.1.0"
diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml
new file mode 100644
index 0000000..c2f2bea
--- /dev/null
+++ b/chart/templates/deployment.yaml
@@ -0,0 +1,38 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .Release.Name }}
+ labels:
+ app: {{ .Release.Name }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ app: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app: {{ .Release.Name }}
+ spec:
+ containers:
+ - name: {{ .Release.Name }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - containerPort: 8000
+ envFrom:
+ - secretRef:
+ name: {{ .Values.secretName }}
+ resources:
+ limits:
+ memory: {{ .Values.resources.limits.memory }}
+ cpu: {{ .Values.resources.limits.cpu }}
+ requests:
+ memory: {{ .Values.resources.requests.memory }}
+ cpu: {{ .Values.resources.requests.cpu }}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8000
+ initialDelaySeconds: 30
+ periodSeconds: 10
diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml
new file mode 100644
index 0000000..3761156
--- /dev/null
+++ b/chart/templates/ingress.yaml
@@ -0,0 +1,20 @@
+{{- if .Values.ingress.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ .Release.Name }}
+ annotations:
+ kubernetes.io/ingress.class: traefik
+spec:
+ rules:
+ - host: {{ .Values.ingress.host }}
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: {{ .Release.Name }}
+ port:
+ number: {{ .Values.service.port }}
+{{- end }}
diff --git a/chart/templates/secret.yaml b/chart/templates/secret.yaml
new file mode 100644
index 0000000..527acf7
--- /dev/null
+++ b/chart/templates/secret.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ .Values.secretName }}
+ annotations:
+ "helm.sh/resource-policy": keep
+type: Opaque
+stringData:
+ IHP_SESSION_SECRET: "REPLACE_ME"
+ DATABASE_URL: "REPLACE_ME"
+ IHP_BASEURL: "http://{{ .Values.ingress.host }}"
diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml
new file mode 100644
index 0000000..06822c6
--- /dev/null
+++ b/chart/templates/service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Release.Name }}
+spec:
+ type: {{ .Values.service.type }}
+ selector:
+ app: {{ .Release.Name }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: {{ .Values.service.targetPort }}
diff --git a/chart/values.yaml b/chart/values.yaml
new file mode 100644
index 0000000..a67424b
--- /dev/null
+++ b/chart/values.yaml
@@ -0,0 +1,25 @@
+image:
+ repository: 92.205.130.254:32166/coulomb/ihp-railiance-probe
+ tag: latest
+ pullPolicy: Always
+
+replicaCount: 1
+
+service:
+ type: ClusterIP
+ port: 80
+ targetPort: 8000
+
+ingress:
+ enabled: true
+ host: probe.coulomb.social
+
+resources:
+ limits:
+ memory: 256Mi
+ cpu: 200m
+ requests:
+ memory: 128Mi
+ cpu: 50m
+
+secretName: ihp-railiance-probe-env
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..8bd1d4a
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,9 @@
+# For backwards compatibility using flake.nix
+(import
+ (
+ fetchTarball {
+ url = "https://github.com/edolstra/flake-compat/archive/35bb57c0c8d8b62bbfd284272c928ceb64ddbde9.tar.gz";
+ sha256 = "sha256:1prd9b1xx8c0sfwnyzkspplh30m613j42l1k789s521f4kv4c2z2";
+ }
+ )
+{ src = ./.; }).defaultNix
diff --git a/devenv.nix b/devenv.nix
new file mode 100644
index 0000000..3848f88
--- /dev/null
+++ b/devenv.nix
@@ -0,0 +1,24 @@
+{ pkgs, lib, config, inputs, ... }:
+{
+ env.GHCRTS = "-A32m -M2g";
+
+ env.IHP_LIB = "${inputs.ihp.packages.${pkgs.stdenv.system}.ihp-env-var-backwards-compat}";
+ env.IHP = "${inputs.ihp.packages.${pkgs.stdenv.system}.ihp-env-var-backwards-compat}";
+
+ languages.haskell.enable = true;
+ languages.haskell.package = pkgs.ghc.ghc.withPackages (p: with p; [
+ p.ihp
+ cabal-install
+ hlint
+ hspec
+ p.ihp-hspec
+ ]);
+ languages.haskell.lsp.enable = false;
+
+ packages = [
+ pkgs.ghc.ghcid
+ pkgs.nodePackages.tailwindcss
+ ];
+
+ processes.tailwind.exec = "tailwindcss -c tailwind/tailwind.config.js -i ./tailwind/app.css -o static/app.css --watch=always";
+}
diff --git a/devenv.yaml b/devenv.yaml
new file mode 100644
index 0000000..0ec6d9f
--- /dev/null
+++ b/devenv.yaml
@@ -0,0 +1,5 @@
+inputs:
+ ihp:
+ url: github:digitallyinduced/ihp/df3922d1a7166b131674efa3d3555ed7195ddf70
+ overlays:
+ - default
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..fdee8d1
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,118 @@
+{
+ inputs = {
+ ihp.url = "github:digitallyinduced/ihp/v1.5";
+ nixpkgs.follows = "ihp/nixpkgs";
+ nixpkgs-nixos.follows = "ihp/nixpkgs-nixos";
+ flake-parts.follows = "ihp/flake-parts";
+ devenv.follows = "ihp/devenv";
+ systems.follows = "ihp/systems";
+ devenv-root = {
+ url = "file+file:///dev/null";
+ flake = false;
+ };
+ };
+
+ outputs = inputs@{ self, nixpkgs, nixpkgs-nixos, ihp, flake-parts, systems, ... }:
+ flake-parts.lib.mkFlake { inherit inputs; } {
+
+ systems = import systems;
+ imports = [ ihp.flakeModules.default ];
+
+ perSystem = { pkgs, config, lib, ... }: {
+ ihp = {
+ appName = "ihp-railiance-probe";
+ enable = true;
+ projectPath = ./.;
+ packages = with pkgs; [
+ tailwindcss
+ ];
+ haskellPackages = p: with p; [
+ p.ihp
+ base
+ wai
+ text
+ ];
+ devHaskellPackages = p: with p; [
+ cabal-install
+ hlint
+ hspec
+ ihp-hspec
+ ];
+
+ withHoogle = false;
+ };
+
+ # OCI container image for Kubernetes deployment.
+ # Build: nix build .#docker
+ # Push: skopeo copy docker-archive:result docker://92.205.130.254:32166/coulomb/ihp-railiance-probe:SHA
+ packages.docker = config.packages.unoptimized-docker-image;
+
+ devenv.shells.default = {
+ overlays = lib.mkAfter [
+ (final: prev: {
+ ghc = prev.ghc.extend (hfinal: hprev: {
+ mkDerivation = args:
+ let drv = hprev.mkDerivation args;
+ in if (args.pname or "") == "ihp-railiance-probe-models"
+ then drv.overrideAttrs (old: {
+ # GHC 9.10.3 Bug 1: Generated.ActualTypes.hi overflow.
+ # Bug 2: libHSghc-9.10.3-5702.a truncated — use ghc-iserv-dyn.
+ configureFlags = (old.configureFlags or []) ++ [
+ "--ghc-option=-O0"
+ "--ghc-option=-fomit-interface-pragmas"
+ "--disable-split-sections"
+ "--ghc-option=-j1"
+ "--ghc-option=-fexternal-interpreter"
+ "--ghc-option=-pgmi"
+ "--ghc-option=${hprev.ghc}/lib/ghc-9.10.3/bin/ghc-iserv-dyn"
+ ];
+ postUnpack = (old.postUnpack or "") + ''
+ _actual="$sourceRoot/build/Generated/ActualTypes.hs"
+ _types=$(
+ {
+ awk '/^data [A-Z]|^newtype [A-Z]/{print $2"(..)"}
+ /^type [A-Z]/{print $2}' \
+ "$sourceRoot/build/Generated/Enums.hs"
+ find "$sourceRoot/build/Generated/ActualTypes" -name "*.hs" | \
+ sort | while IFS= read -r _m; do
+ awk '/^data [A-Z]|^newtype [A-Z]/{print $2"(..)"}
+ /^type [A-Z]/{print $2}' "$_m"
+ done
+ } | sort -u
+ )
+ _exports=$(echo "$_types" | \
+ awk 'NR==1{printf " %s", $0; next} {printf "\n , %s", $0} END{printf "\n"}')
+ _imports=$(awk '/^import Generated\./{print}' "$_actual")
+ {
+ printf 'module Generated.ActualTypes\n ( %s ) where\n' "$_exports"
+ printf '%s\n' "$_imports"
+ } > "$_actual.new" && mv "$_actual.new" "$_actual"
+ '';
+ })
+ else drv;
+ });
+ })
+ ];
+
+ env.GHCRTS = "-A32m -M2g";
+
+ processes = {
+ tailwind.exec = "tailwindcss -c tailwind/tailwind.config.js -i ./tailwind/app.css -o static/app.css --watch=always";
+ };
+ };
+ };
+ };
+
+ nixConfig = {
+ extra-substituters = [
+ "https://devenv.cachix.org"
+ "https://cachix.cachix.org"
+ "https://digitallyinduced.cachix.org"
+ ];
+ extra-trusted-public-keys = [
+ "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="
+ "cachix.cachix.org-1:eWNHQldwUO7G2VkjpnjDbWwy4KQ/HNxht7H4SSoMckM="
+ "digitallyinduced.cachix.org-1:y+wQvrnxQ+PdEsCt91rmvv39qRCYzEgGQaldK26hCKE="
+ ];
+ };
+}
diff --git a/static/.gitkeep b/static/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tailwind/app.css b/tailwind/app.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/tailwind/app.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/tailwind/tailwind.config.js b/tailwind/tailwind.config.js
new file mode 100644
index 0000000..1f57fae
--- /dev/null
+++ b/tailwind/tailwind.config.js
@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ "./Web/View/**/*.hs",
+ "./Web/FrontController.hs",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}