From e223b28e6820dcd9fa5c38ba22de487ada2ca0e6 Mon Sep 17 00:00:00 2001
From: Ben Sima <ben@bsima.me>
Date: Wed, 18 Nov 2020 20:20:27 -0500
Subject: Extend bild to nix targets properly

Also had to capitalize some stuff, and move some nix files around and rename
the metadata directive from 'exe' to 'out' because that just makes more sense,
and fix some compiler errors. But now bild treats both nix and hs files as
buildable things. So that's cool.

One interesting example is Biz/Pie.{nix,hs} - I can either create a dev build of
the hs file with ghc, or I can create a fully-encapsulated nix build. Its nice
to have both options because a dev build with ghc takes half the amount of time,
and I can rely on my locally cached hi and ho files. I think this shows the
power of bild, but also can be a somewhat subtle thing.

The issue really is with the separate command calls in nix builds vs dev builds.
I figure there are a few ways to fix this:

1. Try to use bild inside the nix rules. That could be interesting, but could
also lead to some weird behavior or worm holes forming.
2. Extract the command line invocation into a separate file, some kind of
really simple template that gets pulled into both programs.

It is important to consider that in the future I might want to have bild do a
module-by-module nix build of programs, but I'm not sure how that would effect
my choice here.
---
 .build.yml              |  16 +++--
 Biz/App.hs              |  29 ++++----
 Biz/Bild.hs             |  97 ++++++++++++++-----------
 Biz/Bild/Rules.nix      |  34 +++++----
 Biz/Bild/ShellHook.sh   |   2 +
 Biz/Cloud.nix           |  27 +++++++
 Biz/Dev.nix             |  16 +++++
 Biz/Ibb/Client.hs       |   2 +-
 Biz/Ibb/Server.hs       |   2 +-
 Biz/Pie.hs              |   9 +++
 Biz/Pie.nix             |   1 +
 Hero/Core.hs            | 178 ++++++++++++++++++++++-----------------------
 Hero/Host.hs            |  66 ++++++++---------
 Hero/Look/Typography.hs |   6 +-
 Hero/Node.hs            |   2 +-
 Hero/Prod.nix           |  31 ++++++--
 Que/Apidocs.md          |   3 +
 Que/Client.py           | 186 ++++++++++++++++++++++++++++++++++++++++++++++++
 Que/Host.hs             |   2 +-
 Que/Index.md            |  73 +++++++++++++++++++
 Que/Prod.nix            |  35 +++++++--
 Que/Quescripts.md       |  50 +++++++++++++
 Que/Site.hs             |   2 +-
 Que/Site.nix            |   2 +
 Que/Style.css           | 136 +++++++++++++++++++++++++++++++++++
 Que/Tutorial.md         |  53 ++++++++++++++
 Que/apidocs.md          |   3 -
 Que/client.py           | 186 ------------------------------------------------
 Que/index.md            |  73 -------------------
 Que/quescripts.md       |  50 -------------
 Que/style.css           | 136 -----------------------------------
 Que/tutorial.md         |  53 --------------
 default.nix             |  97 -------------------------
 shell.nix               |   4 +-
 34 files changed, 848 insertions(+), 814 deletions(-)
 create mode 100644 Biz/Cloud.nix
 create mode 100644 Biz/Dev.nix
 create mode 100644 Biz/Pie.nix
 create mode 100644 Que/Apidocs.md
 create mode 100755 Que/Client.py
 create mode 100644 Que/Index.md
 create mode 100644 Que/Quescripts.md
 create mode 100644 Que/Style.css
 create mode 100644 Que/Tutorial.md
 delete mode 100644 Que/apidocs.md
 delete mode 100755 Que/client.py
 delete mode 100644 Que/index.md
 delete mode 100644 Que/quescripts.md
 delete mode 100644 Que/style.css
 delete mode 100644 Que/tutorial.md
 delete mode 100644 default.nix

diff --git a/.build.yml b/.build.yml
index adb3fd8..00c0cc8 100644
--- a/.build.yml
+++ b/.build.yml
@@ -6,7 +6,15 @@ secrets:
 tasks:
     - build: |
         cd biz
-        nix-build ./default.nix -A Biz.Cloud -A Biz.Dev
-    - nix-shell: |
-        cd biz
-        nix-shell --run "echo okay"
+        nix-shell --run "bild Biz/Pie.hs"
+        nix-shell --run "bild Biz/Pie.nix"
+        nix-shell --run "bild Biz/Cloud.nix"
+        nix-shell --run "bild Biz/Dev.nix"
+
+        nix-shell --run "bild Que/Site.hs"
+        nix-shell --run "bild Que/Host.hs"
+        nix-shell --run "bild Que/Prod.nix"
+
+        nix-shell --run "bild Hero/Host.hs"
+        nix-shell --run "bild Hero/Node.hs"
+        nix-shell --run "bild Hero/Prod.nix"
diff --git a/Biz/App.hs b/Biz/App.hs
index 95e7271..d16bba9 100644
--- a/Biz/App.hs
+++ b/Biz/App.hs
@@ -1,9 +1,10 @@
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
 
 -- | General utils for apps
-module Biz.App (CSS(..), Manifest(..)) where
+module Biz.App (CSS (..), Manifest (..)) where
 
 import Alpha
 import Data.Aeson (ToJSON)
@@ -15,10 +16,9 @@ import Network.HTTP.Media
   )
 import Servant
 
-newtype CSS
-  = CSS
-      { unCSS :: Text
-      }
+newtype CSS = CSS
+  { unCSS :: Text
+  }
 
 instance Accept CSS where
   contentType _ = "text" // "css" /: ("charset", "utf-8")
@@ -27,17 +27,14 @@ instance MimeRender CSS Text where
   mimeRender _ = Lazy.encodeUtf8 . Lazy.fromStrict
 
 -- | The manifest describes your app for web app thumbnails, iPhone tiles, etc.
-data Manifest
-  = Manifest
-      { name :: Text,
-        short_name :: Text,
-        start_url :: Text,
-        display :: Text,
-        theme_color :: Text,
-        description :: Text
-      }
+data Manifest = Manifest
+  { name :: Text,
+    short_name :: Text,
+    start_url :: Text,
+    display :: Text,
+    theme_color :: Text,
+    description :: Text
+  }
   deriving (Show, Eq, Generic)
 
 instance ToJSON Manifest
-
-
diff --git a/Biz/Bild.hs b/Biz/Bild.hs
index 92054f9..169fd6f 100644
--- a/Biz/Bild.hs
+++ b/Biz/Bild.hs
@@ -64,7 +64,7 @@
 --
 -- > bild A/B.hs
 --
--- This will build the file at ./A/B.hs, this will translate to something like
+-- This will build the file at ./A/B.hs, which translates to something like
 -- `ghc --make A.B`.
 --
 -- > bild -s <target>
@@ -94,16 +94,16 @@
 --
 -- The output executable is named with:
 --
--- > -- : exe my-program
+-- > -- : out my-program
 --
 -- or
 --
--- > -- : exe my-ap.js
+-- > -- : out my-ap.js
 --
 -- When multiple compilers are possible (e.g. ghc vs ghcjs) we chose ghcjs when
--- the target exe ends in .js.
+-- the target @out@ ends in .js.
 --
--- This method of setting metadata in the module comments  works pretty well,
+-- This method of setting metadata in the module comments works pretty well,
 -- and really only needs to be done in the entrypoint module anyway.
 --
 -- Local module deps are included by just giving the repo root to the compiler
@@ -143,14 +143,14 @@ type Namespace = String
 
 type Dep = String
 
-type Exe = String
+type Out = String
 
 data Compiler = Ghc | Ghcjs | Nix
   deriving (Show)
 
 data Target = Target
-  { -- | Output executable name
-    exe :: Exe,
+  { -- | Output name
+    out :: Out,
     -- | Fully qualified namespace partitioned by '.'
     namespace :: Namespace,
     -- | Absolute path to file
@@ -170,24 +170,30 @@ analyze s = do
   case File.takeExtension path of
     ".hs" -> do
       content <- String.lines </ Prelude.readFile path
-      let exe = content /> Regex.match metaExe |> catMaybes |> head |> require "exe"
-      let compiler = if ".js" `List.isSuffixOf` exe then Ghcjs else Ghc
-      return Target {
-        namespace = require "namespace"
-          <| path
-          |> reps root ""
-          |> File.dropExtension
-          |> reps "/" "."
-          |> List.stripPrefix "."
-          >>= Regex.match metaNamespace,
-        deps = content /> Regex.match metaDep |> catMaybes,
-        ..
-      }
-
-    ".nix" -> return Target {
-        namespace = s, path = path, deps = [], compiler = Nix, exe = ""
-      }
-
+      let out = content /> Regex.match metaOut |> catMaybes |> head |> require "out"
+      let compiler = if ".js" `List.isSuffixOf` out then Ghcjs else Ghc
+      return
+        Target
+          { namespace =
+              require "namespace"
+                <| path
+                |> reps root ""
+                |> File.dropExtension
+                |> reps "/" "."
+                |> List.stripPrefix "."
+                >>= Regex.match metaNamespace,
+            deps = content /> Regex.match metaDep |> catMaybes,
+            ..
+          }
+    ".nix" ->
+      return
+        Target
+          { namespace = s,
+            path = path,
+            deps = [],
+            compiler = Nix,
+            out = ""
+          }
     e -> panic <| "bild does not know this extension: " <> Text.pack e
 
 build :: Target -> IO ()
@@ -196,8 +202,8 @@ build Target {..} = do
   case compiler of
     Ghc -> do
       putText <| "bild: ghc: " <> Text.pack namespace
-      let out = root </> "_/bild/dev/bin"
-      Dir.createDirectoryIfMissing True out
+      let devOut = root </> "_/bild/dev/bin"
+      Dir.createDirectoryIfMissing True devOut
       Process.callProcess
         "ghc"
         [ "-Werror",
@@ -211,12 +217,12 @@ build Target {..} = do
           "-main-is",
           namespace,
           "-o",
-          out </> exe
+          devOut </> out
         ]
     Ghcjs -> do
       putText <| "bild: ghcjs: " <> Text.pack namespace
-      let out = root </> "_/bild/dev/static"
-      Dir.createDirectoryIfMissing True out
+      let devOut = root </> "_/bild/dev/static"
+      Dir.createDirectoryIfMissing True devOut
       Process.callProcess
         "ghcjs"
         [ "-Werror",
@@ -230,19 +236,30 @@ build Target {..} = do
           "-main-is",
           namespace,
           "-o",
-          out </> exe
+          devOut </> out
         ]
     Nix -> do
       putText <| "bild: nix: " <> Text.pack namespace
       cwd <- Dir.getCurrentDirectory
-      let qualifiedTarget = reps root "" cwd <> namespace
+      let nixOut = root </> "_/bild/nix"
+      Dir.createDirectoryIfMissing True nixOut
+      let qualifiedTarget = reps root "" cwd </> namespace
       Process.callProcess
         "nix-build"
-        [ "-o",
-          root </> "_/bild/nix" </> qualifiedTarget,
-          root </> "default.nix",
-          "--attr",
-          qualifiedTarget
+        [ path,
+          "-o",
+          nixOut </> qualifiedTarget,
+          "--arg",
+          "bild",
+          "import " <> root
+            </> "Biz/Bild/Rules.nix"
+            <> " { nixpkgs = import "
+            <> root
+            </> "Biz/Bild/Nixpkgs.nix"
+            <> "; }",
+          "--arg",
+          "lib",
+          "(import " <> root </> "Biz/Bild/Nixpkgs.nix).lib"
         ]
 
 metaNamespace :: Regex.RE Char Namespace
@@ -253,8 +270,8 @@ metaNamespace = name <> Regex.many (Regex.sym '.') <> name
 metaDep :: Regex.RE Char Dep
 metaDep = Regex.string "-- : dep " *> Regex.many (Regex.psym Char.isAlpha)
 
-metaExe :: Regex.RE Char Exe
-metaExe = Regex.string "-- : exe " *> Regex.many (Regex.psym (/= ' '))
+metaOut :: Regex.RE Char Out
+metaOut = Regex.string "-- : out " *> Regex.many (Regex.psym (/= ' '))
 
 require :: Text -> Maybe a -> a
 require _ (Just x) = x
diff --git a/Biz/Bild/Rules.nix b/Biz/Bild/Rules.nix
index e76d7a2..2a1a4d1 100644
--- a/Biz/Bild/Rules.nix
+++ b/Biz/Bild/Rules.nix
@@ -28,9 +28,9 @@ let
     module = builtins.replaceStrings ["/" ".hs"] ["." ""] relpath;
     # file contents
     content = builtins.readFile main;
-    # search for the ': exe' declaration
-    exe = builtins.head (lib.lists.flatten (removeNull
-      (map (builtins.match "^-- : exe ([[:alnum:]._-]*)$")
+    # search for the ': out' declaration
+    out = builtins.head (lib.lists.flatten (removeNull
+      (map (builtins.match "^-- : out ([[:alnum:]._-]*)$")
         (lines content))));
     # collect all of the ': dep' declarations
     deps = lib.lists.flatten (removeNull
@@ -38,7 +38,7 @@ let
         (lines content)));
 
     sysdeps = lib.lists.flatten (removeNull
-      (map (builtins.match "^-- : sys ([[:alum:]._-]*)$")
+      (map (builtins.match "^-- : sys ([[:alnum:]._-]*)$")
         (lines content)));
   };
 
@@ -59,16 +59,18 @@ in {
       ghc = ghc_ data.deps;
     in stdenv.mkDerivation {
       name = data.module;
-      src = ../.;
+      src = ../../.;
       nativeBuildInputs = [ ghc ] ++ depsToPackageSet nixpkgs data.sysdeps;
       strictDeps = true;
       buildPhase = ''
         mkdir -p $out/bin
         # compile with ghc
-        ${ghc}/bin/ghc -Werror -Weverything -i. \
-            --make ${main} \
-            -main-is ${data.module} \
-            -o $out/bin/${data.exe}
+        ${ghc}/bin/ghc \
+          -Werror \
+          -i. \
+          --make ${main} \
+          -main-is ${data.module} \
+          -o $out/bin/${data.out}
       '';
       # the install process was handled above
       installPhase = "exit 0";
@@ -80,19 +82,21 @@ in {
       ghcjs = ghcjs_ data.deps;
     in stdenv.mkDerivation {
       name = data.module;
-      src = ../.;
+      src = ../../.;
       nativeBuildInputs = [ ghcjs ];
       strictDeps = true;
       buildPhase = ''
         mkdir -p $out/static
         # compile with ghcjs
-        ${ghcjs}/bin/ghcjs -Werror -Weverything -i. \
-            --make ${main} \
-            -main-is ${data.module} \
-            -o ${data.exe}
+        ${ghcjs}/bin/ghcjs \
+          -Werror \
+          -i. \
+          --make ${main} \
+          -main-is ${data.module} \
+          -o ${data.out}
         # optimize js output
         ${pkgs.closurecompiler}/bin/closure-compiler \
-          ${data.exe}/all.js > $out/static/${data.exe}
+          ${data.out}/all.js > $out/static/${data.out}
       '';
       installPhase = "exit 0";
     } // { env = ghcjs; };
diff --git a/Biz/Bild/ShellHook.sh b/Biz/Bild/ShellHook.sh
index 89751d3..0fc1781 100644
--- a/Biz/Bild/ShellHook.sh
+++ b/Biz/Bild/ShellHook.sh
@@ -13,6 +13,8 @@ function help() {
   echo "   ship   lint, bild, and push one (or all) namespace(s)"
 }
 
+alias runghc="runghc --ghc-arg=-i$BIZ_ROOT"
+
 function bild() {
   runghc Biz.Bild $@
 }
diff --git a/Biz/Cloud.nix b/Biz/Cloud.nix
new file mode 100644
index 0000000..edf8a85
--- /dev/null
+++ b/Biz/Cloud.nix
@@ -0,0 +1,27 @@
+{ bild }:
+
+# Cloud infrastructure, always online. Mostly for messaging-related stuff.
+
+let
+  nixos-mailserver = let ver = "v2.3.0"; in builtins.fetchTarball {
+    url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${ver}/nixos-mailserver-${ver}.tar.gz";
+    sha256 = "0lpz08qviccvpfws2nm83n7m2r8add2wvfg9bljx9yxx8107r919";
+  };
+in
+bild.os {
+  imports = [
+    ./OsBase.nix
+    ./Packages.nix
+    ./Users.nix
+    ./Cloud/Chat.nix
+    ./Cloud/Git.nix
+    ./Cloud/Hardware.nix
+    ./Cloud/Mail.nix
+    ./Cloud/Networking.nix
+    ./Cloud/Web.nix
+    ./Cloud/Znc.nix
+    nixos-mailserver
+  ];
+  networking.hostName = "simatime";
+  networking.domain = "simatime.com";
+}
diff --git a/Biz/Dev.nix b/Biz/Dev.nix
new file mode 100644
index 0000000..a08a8f7
--- /dev/null
+++ b/Biz/Dev.nix
@@ -0,0 +1,16 @@
+{ bild }:
+
+# Dev machine for work and building stuff.
+
+bild.os {
+  imports = [
+    ./OsBase.nix
+    ./Packages.nix
+    ./Users.nix
+    ./Dev/Configuration.nix
+    ./Dev/Hardware.nix
+  ];
+  networking.hostName = "lithium";
+  networking.domain = "dev.simatime.com";
+}
+
diff --git a/Biz/Ibb/Client.hs b/Biz/Ibb/Client.hs
index c3dae4b..d0ed3e3 100644
--- a/Biz/Ibb/Client.hs
+++ b/Biz/Ibb/Client.hs
@@ -3,7 +3,7 @@
 
 -- | Front-end
 --
--- : exe ibb.js
+-- : out ibb.js
 --
 -- : dep clay
 -- : dep miso
diff --git a/Biz/Ibb/Server.hs b/Biz/Ibb/Server.hs
index d7b4969..058bbdc 100644
--- a/Biz/Ibb/Server.hs
+++ b/Biz/Ibb/Server.hs
@@ -7,7 +7,7 @@
 
 -- | Server
 --
--- : exe ibb
+-- : out ibb
 --
 -- : dep clay
 -- : dep miso
diff --git a/Biz/Pie.hs b/Biz/Pie.hs
index 7e1c19e..409f14f 100644
--- a/Biz/Pie.hs
+++ b/Biz/Pie.hs
@@ -29,6 +29,15 @@
 --   - Sean Ellis' question: "How would you feel if you could no longer use this
 --     product? (a) Very disappointed, (b) somewhat disappointed, (c) not
 --     disappointed" and then measure the percentage who answer (a).
+--
+-- Bild Metadata:
+--
+-- : out pie
+-- : dep aeson
+-- : dep protolude
+-- : dep optparse-simple
+-- : dep parsec
+-- : dep haskeline
 module Biz.Pie
   ( main,
   )
diff --git a/Biz/Pie.nix b/Biz/Pie.nix
new file mode 100644
index 0000000..70e2f23
--- /dev/null
+++ b/Biz/Pie.nix
@@ -0,0 +1 @@
+{ bild }: bild.ghc ./Pie.hs
diff --git a/Hero/Core.hs b/Hero/Core.hs
index bc53503..8f865da 100644
--- a/Hero/Core.hs
+++ b/Hero/Core.hs
@@ -129,14 +129,14 @@ data Button
 class Elemental v where el :: v -> View Move
 
 -- TODO: what if I just did this on all actions?
--- then I could e.g. `el $ ToggleAudio audioId audioState`
+-- then I could e.g. `el <| ToggleAudio audioId audioState`
 instance Elemental Button where
   el (PlayPause id form) =
     button_
       [ class_ "button is-large icon",
-        onClick $ ToggleAudio id
+        onClick <| ToggleAudio id
       ]
-      [i_ [class_ $ "fa " <> icon] []]
+      [i_ [class_ <| "fa " <> icon] []]
     where
       icon = case form of
         Paused -> "fa-play-circle"
@@ -144,7 +144,7 @@ instance Elemental Button where
   el (Arrow act) =
     button_
       [class_ "button is-large turn-page", onClick act]
-      [img_ [src_ $ ms $ Pack.demo <> image <> ".png"]]
+      [img_ [src_ <| ms <| Pack.demo <> image <> ".png"]]
     where
       image = case act of
         PrevPage -> "prev-page"
@@ -154,15 +154,15 @@ instance Elemental Button where
     if c `elem` userLibrary u -- in library
       then
         a_
-          [class_ "wrs-button saved", onClick $ ToggleInLibrary c]
-          [ img_ [src_ $ ms $ Pack.icon <> "save.svg"],
+          [class_ "wrs-button saved", onClick <| ToggleInLibrary c]
+          [ img_ [src_ <| ms <| Pack.icon <> "save.svg"],
             span_ [] [text "saved"]
           ]
       else-- not in library
 
         a_
-          [class_ "wrs-button", onClick $ ToggleInLibrary c]
-          [ img_ [src_ $ ms $ Pack.icon <> "save.svg"],
+          [class_ "wrs-button", onClick <| ToggleInLibrary c]
+          [ img_ [src_ <| ms <| Pack.icon <> "save.svg"],
             span_ [] [text "save"]
           ]
   el (SaveIcon c u) =
@@ -170,46 +170,46 @@ instance Elemental Button where
       then
         button_
           [ class_ "button is-large has-background-black",
-            onClick $ ToggleInLibrary c
+            onClick <| ToggleInLibrary c
           ]
-          [img_ [src_ $ ms $ Pack.demo <> "library-add.png"]]
+          [img_ [src_ <| ms <| Pack.demo <> "library-add.png"]]
       else-- not in library
 
         button_
           [ class_ "button is-large has-background-black-bis",
-            onClick $ ToggleInLibrary c
+            onClick <| ToggleInLibrary c
           ]
-          [img_ [src_ $ ms $ Pack.demo <> "library-add.png"]]
+          [img_ [src_ <| ms <| Pack.demo <> "library-add.png"]]
   el (ZoomIcon zform comic page) =
     button_
       [ id_ "zoom-button",
         class_ "button is-large",
-        onClick $ ToggleZoom comic page
+        onClick <| ToggleZoom comic page
       ]
-      [ img_ [src_ $ ms $ Pack.demo <> "zoom.png"],
+      [ img_ [src_ <| ms <| Pack.demo <> "zoom.png"],
         input_
           [ type_ "range",
             min_ "0",
             max_ "100",
             disabled_ True,
-            value_ $ ms (show zform :: String),
+            value_ <| ms (show zform :: String),
             class_ "ctrl",
             id_ "zoom"
           ],
         label_
           [class_ "ctrl", Miso.for_ "zoom"]
-          [text $ ms $ (show zform :: String) ++ "%"]
+          [text <| ms <| (show zform :: String) ++ "%"]
       ]
   el (Read c) =
     a_
-      [class_ "wrs-button", onClick $ SelectExperience c]
-      [ img_ [src_ $ ms $ Pack.icon <> "read.svg"],
+      [class_ "wrs-button", onClick <| SelectExperience c]
+      [ img_ [src_ <| ms <| Pack.icon <> "read.svg"],
         span_ [] [text "read"]
       ]
   el (Watch c) =
     a_
-      [class_ "wrs-button", onClick $ StartWatching c]
-      [ img_ [src_ $ ms $ Pack.icon <> "watch.svg"],
+      [class_ "wrs-button", onClick <| StartWatching c]
+      [ img_ [src_ <| ms <| Pack.icon <> "watch.svg"],
         span_ [] [text "watch"]
       ]
 
@@ -257,12 +257,12 @@ initForm uri_ =
 
 -- | Hacky way to initialize the 'ComicReaderState' from the Api.URI.
 detectPlayerState :: Api.URI -> ComicReaderState
-detectPlayerState u = case List.splitOn "/" $ Api.uriPath u of
+detectPlayerState u = case List.splitOn "/" <| Api.uriPath u of
   ["", "comic", id, pg, "experience"] -> ChooseExperience (ComicId id) (toPage pg)
-  ["", "comic", id, _, "video"] -> Watching $ ComicId id
+  ["", "comic", id, _, "video"] -> Watching <| ComicId id
   ["", "comic", id, pg, "full"] -> Reading Full (ComicId id) (toPage pg)
   ["", "comic", id, pg] -> Reading Spread (ComicId id) (toPage pg)
-  ["", "comic", id] -> Cover $ ComicId id
+  ["", "comic", id] -> Cover <| ComicId id
   _ -> NotReading
   where
     toPage pg = fromMaybe 1 (readMaybe pg :: Maybe PageNumber)
@@ -344,7 +344,7 @@ homeProxy :: Proxy Home
 homeProxy = Proxy
 
 homeLink :: Api.URI
-homeLink = linkURI $ Api.safeLink front homeProxy
+homeLink = linkURI <| Api.safeLink front homeProxy
   where
     front = Proxy :: Proxy Home
 
@@ -367,7 +367,7 @@ loginProxy :: Proxy Login
 loginProxy = Proxy
 
 loginLink :: Api.URI
-loginLink = linkURI $ Api.safeLink pubRoutes loginProxy
+loginLink = linkURI <| Api.safeLink pubRoutes loginProxy
 
 login :: form -> View Move
 login _ =
@@ -377,7 +377,7 @@ login _ =
         [id_ "login-inner"]
         [ img_
             [ class_ fadeIn,
-              src_ $ ms $ Pack.cdnEdge <> "/old-assets/images/icons/hero-large.png"
+              src_ <| ms <| Pack.cdnEdge <> "/old-assets/images/icons/hero-large.png"
             ],
           hr_ [class_ fadeIn],
           form_
@@ -398,14 +398,14 @@ login _ =
             ],
           hr_ [class_ fadeIn],
           p_
-            [class_ $ "help " <> fadeIn]
+            [class_ <| "help " <> fadeIn]
             [ a_ [href_ "#"] [text "Forgot your username or password?"],
               a_ [href_ "#"] [text "Don't have an account? Sign Up"]
             ],
           img_
             [ id_ "hero-logo",
               class_ "blur-out",
-              src_ $ ms $ Pack.cdnEdge <> "/old-assets/images/icons/success-her-image.png"
+              src_ <| ms <| Pack.cdnEdge <> "/old-assets/images/icons/success-her-image.png"
             ]
         ]
     ]
@@ -418,7 +418,7 @@ login _ =
 type Discover = "discover" :> View Move
 
 discoverLink :: Api.URI
-discoverLink = linkURI $ Api.safeLink routes discoverProxy
+discoverLink = linkURI <| Api.safeLink routes discoverProxy
 
 discoverProxy :: Proxy Discover
 discoverProxy = Proxy
@@ -428,7 +428,7 @@ discover form@Form {user = u} =
   template
     "discover"
     [ topbar,
-      main_ [id_ "app-body"] $ case appComics form of
+      main_ [id_ "app-body"] <| case appComics form of
         NotAsked -> [loading]
         Loading -> [loading]
         Failure _ -> [nocomics]
@@ -436,7 +436,7 @@ discover form@Form {user = u} =
         Success (comic : rest) ->
           [ feature comic u,
             shelf "Recent Releases" (comic : rest),
-            maybeView (`info` u) $ dMediaInfo form
+            maybeView (`info` u) <| dMediaInfo form
           ],
       appmenu,
       discoverFooter
@@ -452,11 +452,11 @@ discoverFooter =
         [id_ "app-foot-social", css euro]
         [ div_
             [class_ "row is-marginless"]
-            [ smallImg "facebook.png" $ Just "https://www.facebook.com/musicmeetscomics",
-              smallImg "twitter.png" $ Just "https://twitter.com/musicmeetscomic",
-              smallImg "instagram.png" $ Just "https://www.instagram.com/musicmeetscomics/",
-              smallImg "spotify.png" $ Just "https://open.spotify.com/user/i4ntfg6ganjgxdsylinigcjlq?si=ymWsSkwsT9iaLw2LeAJNNg",
-              smallImg "youtube.png" $ Just "https://www.youtube.com/channel/UCnNPLiuJ1ueo1KTPgHDE7lA/"
+            [ smallImg "facebook.png" <| Just "https://www.facebook.com/musicmeetscomics",
+              smallImg "twitter.png" <| Just "https://twitter.com/musicmeetscomic",
+              smallImg "instagram.png" <| Just "https://www.instagram.com/musicmeetscomics/",
+              smallImg "spotify.png" <| Just "https://open.spotify.com/user/i4ntfg6ganjgxdsylinigcjlq?si=ymWsSkwsT9iaLw2LeAJNNg",
+              smallImg "youtube.png" <| Just "https://www.youtube.com/channel/UCnNPLiuJ1ueo1KTPgHDE7lA/"
             ],
           div_ [class_ "row"] [text "Team | Contact Us | Privacy Policy"]
         ],
@@ -467,7 +467,7 @@ discoverFooter =
         ],
       div_
         [css euro, id_ "app-foot-logo", onClick Dumpform]
-        [ a_ [class_ "social-icon", href_ "#"] [img_ [src_ $ ms $ Pack.icon <> "hero-logo.svg"]],
+        [ a_ [class_ "social-icon", href_ "#"] [img_ [src_ <| ms <| Pack.icon <> "hero-logo.svg"]],
           span_ [] [text "© Hero Records, Inc. All Rights Reserved"]
         ]
     ]
@@ -477,7 +477,7 @@ discoverFooter =
     smallImg x lnk =
       a_
         (attrs lnk)
-        [img_ [src_ $ ms $ Pack.cdnEdge <> "/old-assets/images/icons/" <> x]]
+        [img_ [src_ <| ms <| Pack.cdnEdge <> "/old-assets/images/icons/" <> x]]
 
 -- ** comic
 
@@ -504,12 +504,12 @@ instance IsMediaObject Comic where
       []
       [ a_
           [ class_ "comic grow clickable",
-            id_ $ "comic-" <> ms comicId,
-            onClick $ SetMediaInfo $ Just c
+            id_ <| "comic-" <> ms comicId,
+            onClick <| SetMediaInfo <| Just c
           ]
-          [ img_ [src_ $ ms $ Pack.demo <> comicSlug c <> ".png"],
-            span_ [] [text $ "Issue #" <> ms comicIssue],
-            span_ [] [text $ ms comicName]
+          [ img_ [src_ <| ms <| Pack.demo <> comicSlug c <> ".png"],
+            span_ [] [text <| "Issue #" <> ms comicIssue],
+            span_ [] [text <| ms comicName]
           ]
       ]
   feature comic lib =
@@ -517,7 +517,7 @@ instance IsMediaObject Comic where
       [id_ "featured-comic"]
       [ img_
           [ id_ "featured-banner",
-            src_ $ ms $ Pack.demo <> "feature-banner.png"
+            src_ <| ms <| Pack.demo <> "feature-banner.png"
           ],
         div_
           [id_ "featured-content"]
@@ -530,15 +530,15 @@ instance IsMediaObject Comic where
               [class_ "comic-logo"]
               [ img_
                   [ src_
-                      $ ms
-                      $ Pack.demo <> comicSlug comic <> "-logo.png"
+                      <| ms
+                      <| Pack.demo <> comicSlug comic <> "-logo.png"
                   ]
               ],
-            div_ [class_ "comic-action-menu"] $
-              el <$> [Watch comic, Read comic, Save comic lib],
+            div_ [class_ "comic-action-menu"] <|
+              el </ [Watch comic, Read comic, Save comic lib],
             p_
               [class_ "description"]
-              [ text . ms $ comicDescription comic
+              [ text . ms <| comicDescription comic
               ]
           ]
       ]
@@ -547,22 +547,22 @@ instance IsMediaObject Comic where
       [class_ "media-info", css euro]
       [ div_
           [class_ "media-info-meta"]
-          [ column [img_ [src_ $ ms $ Pack.demo <> "dmc-widethumb.png"]],
+          [ column [img_ [src_ <| ms <| Pack.demo <> "dmc-widethumb.png"]],
             column
-              [ span_ [style_ title] [text $ ms comicName],
-                span_ [style_ subtitle] [text $ "Issue #" <> ms comicIssue],
+              [ span_ [style_ title] [text <| ms comicName],
+                span_ [style_ subtitle] [text <| "Issue #" <> ms comicIssue],
                 span_ [] [text "Released: "],
-                span_ [] [text $ "Pages: " <> ms (show comicPages :: String)]
+                span_ [] [text <| "Pages: " <> ms (show comicPages :: String)]
               ]
           ],
         div_
           [class_ "media-info-summary"]
           [ p_
-              [style_ $ uppercase <> bold <> Look.expanded <> "font-size" =: ".8rem"]
+              [style_ <| uppercase <> bold <> Look.expanded <> "font-size" =: ".8rem"]
               [text "Summary"],
-            p_ [] [text $ ms comicDescription]
+            p_ [] [text <| ms comicDescription]
           ],
-        div_ [class_ "media-info-actions"] $ el <$> [Save c lib, Read c, Watch c]
+        div_ [class_ "media-info-actions"] <| el </ [Save c lib, Read c, Watch c]
         -- , row [ text "credits" ]
       ]
     where
@@ -586,7 +586,7 @@ comicCover :: ComicId -> Form -> View Move
 comicCover comicId_ = comicReader comicId_ 1
 
 comicLink :: ComicId -> Api.URI
-comicLink comicId_ = linkURI $ Api.safeLink routes comicProxy comicId_
+comicLink comicId_ = linkURI <| Api.safeLink routes comicProxy comicId_
 
 -- ** chooseExperience
 
@@ -602,7 +602,7 @@ chooseExperienceProxy = Proxy
 
 chooseExperienceLink :: ComicId -> PageNumber -> Api.URI
 chooseExperienceLink id page =
-  linkURI $ Api.safeLink routes chooseExperienceProxy id page
+  linkURI <| Api.safeLink routes chooseExperienceProxy id page
 
 chooseExperiencePage :: Comic -> PageNumber -> Form -> View Move
 chooseExperiencePage comic page form =
@@ -613,7 +613,7 @@ chooseExperiencePage comic page form =
         [id_ "app-body"]
         [ h2_ [] [text "Choose Your Musical Experience"],
           p_ [] [text experienceBlurb],
-          ul_ [] $ li comic </ experiences
+          ul_ [] <| li comic </ experiences
         ],
       appmenu,
       comicControls comic page form
@@ -621,14 +621,14 @@ chooseExperiencePage comic page form =
   where
     li c (name, artist, track) =
       li_
-        [onClick $ StartReading c]
+        [onClick <| StartReading c]
         [ div_
             []
-            [ img_ [src_ $ ms $ Pack.demo <> name <> ".png"],
-              span_ [] [text $ ms name]
+            [ img_ [src_ <| ms <| Pack.demo <> name <> ".png"],
+              span_ [] [text <| ms name]
             ],
-          span_ [css thicc] [text $ ms artist],
-          span_ [] [text $ ms track]
+          span_ [css thicc] [text <| ms artist],
+          span_ [] [text <| ms track]
         ]
     experiences :: [(Text, Text, Text)]
     experiences =
@@ -703,7 +703,7 @@ comicReaderSpreadProxy = Proxy
 
 comicReaderSpreadLink :: ComicId -> PageNumber -> Api.URI
 comicReaderSpreadLink id page =
-  linkURI $ Api.safeLink routes comicReaderSpreadProxy id page
+  linkURI <| Api.safeLink routes comicReaderSpreadProxy id page
 
 comicSpread :: Comic -> PageNumber -> Form -> View Move
 comicSpread comic page form =
@@ -740,7 +740,7 @@ comicSpread comic page form =
 closeButton :: View Move
 closeButton =
   a_
-    [id_ "close-button", onClick $ ChangeURI discoverLink]
+    [id_ "close-button", onClick <| ChangeURI discoverLink]
     [text "x"]
 
 -- * comicReaderFull
@@ -757,7 +757,7 @@ comicReaderFullProxy = Proxy
 
 comicReaderFullLink :: ComicId -> PageNumber -> Api.URI
 comicReaderFullLink id page =
-  linkURI $ Api.safeLink routes comicReaderFullProxy id page
+  linkURI <| Api.safeLink routes comicReaderFullProxy id page
 
 -- * comicVideo
 
@@ -773,7 +773,7 @@ comicVideoProxy = Proxy
 
 comicVideoLink :: ComicId -> PageNumber -> Api.URI
 comicVideoLink id page =
-  linkURI $ Api.safeLink routes comicVideoProxy id page
+  linkURI <| Api.safeLink routes comicVideoProxy id page
 
 frameborder_ :: MisoString -> Attribute action
 frameborder_ = textProp "frameborder"
@@ -812,7 +812,7 @@ mediaInfo (Just comic) user =
   div_ [class_ "media-info"] [info comic user]
 
 appmenu :: View Move
-appmenu = aside_ [id_ "appmenu"] $ btn </ links
+appmenu = aside_ [id_ "appmenu"] <| btn </ links
   where
     links =
       -- these extra 'discoverLink's are just dummies
@@ -825,9 +825,9 @@ appmenu = aside_ [id_ "appmenu"] $ btn </ links
     btn (lnk, img, label) =
       a_
         [ class_ "button",
-          onPreventClick $ ChangeURI lnk
+          onPreventClick <| ChangeURI lnk
         ]
-        [ img_ [src_ $ ms $ Pack.icon <> img],
+        [ img_ [src_ <| ms <| Pack.icon <> img],
           span_ [] [text label]
         ]
 
@@ -844,7 +844,7 @@ shelf title comics =
   div_
     [class_ "shelf"]
     [ div_ [class_ "shelf-head"] [text title],
-      ul_ [class_ "shelf-body"] $ thumbnail </ comics
+      ul_ [class_ "shelf-body"] <| thumbnail </ comics
     ]
 
 viewOr404 ::
@@ -865,7 +865,7 @@ template id = div_ [id_ id, class_ "app is-black"]
 padLeft :: Int -> MisoString
 padLeft n
   | n < 10 = ms ("0" <> Legacy.show n)
-  | otherwise = ms $ Legacy.show n
+  | otherwise = ms <| Legacy.show n
 
 comicControls :: Comic -> PageNumber -> Form -> View Move
 comicControls comic page form =
@@ -877,31 +877,31 @@ comicControls comic page form =
         ]
         [ audio_
             [id_ audioId, loop_ True, crossorigin_ "anonymous"]
-            [source_ [src_ $ ms $ Pack.demo <> "stars-instrumental.mp3"]],
-          el $ PlayPause audioId $ cpAudioState form,
+            [source_ [src_ <| ms <| Pack.demo <> "stars-instrumental.mp3"]],
+          el <| PlayPause audioId <| cpAudioState form,
           span_
-            [css $ euro <> thicc <> smol <> wide]
+            [css <| euro <> thicc <> smol <> wide]
             [text "Experiencing: Original"]
         ],
       div_
         [class_ "comic-controls-pages", css euro]
-        [ el $ Arrow PrevPage,
-          span_ [] [text $ leftPage <> "-" <> rightPage <> " of " <> totalpages],
-          el $ Arrow NextPage
+        [ el <| Arrow PrevPage,
+          span_ [] [text <| leftPage <> "-" <> rightPage <> " of " <> totalpages],
+          el <| Arrow NextPage
         ],
       div_
         [class_ "comic-controls-share"]
-        [ el $ SaveIcon comic $ user form,
-          el $ ZoomIcon (magnification form) comic page,
+        [ el <| SaveIcon comic <| user form,
+          el <| ZoomIcon (magnification form) comic page,
           button_
             [class_ "button icon is-large", onClick ToggleFullscreen]
             [i_ [class_ "fa fa-expand"] []]
         ]
     ]
   where
-    leftPage = ms . Legacy.show $ page
-    rightPage = ms . Legacy.show $ 1 + page
-    totalpages = ms . Legacy.show $ comicPages comic
+    leftPage = ms . Legacy.show <| page
+    rightPage = ms . Legacy.show <| 1 + page
+    totalpages = ms . Legacy.show <| comicPages comic
 
 topbar :: View Move
 topbar =
@@ -909,9 +909,9 @@ topbar =
     [id_ "app-head", class_ "is-black", css euro]
     [ a_
         [ class_ "button is-medium is-black",
-          onClick $ ChangeURI discoverLink
+          onClick <| ChangeURI discoverLink
         ]
-        [img_ [src_ $ ms $ Pack.icon <> "hero-logo.svg"]],
+        [img_ [src_ <| ms <| Pack.icon <> "hero-logo.svg"]],
       div_
         [id_ "app-head-right"]
         [ button_
@@ -919,7 +919,7 @@ topbar =
             [i_ [class_ "fas fa-search"] []],
           button_
             [ class_ "button is-medium is-black is-size-7",
-              css $ euro <> wide <> thicc
+              css <| euro <> wide <> thicc
             ]
             [text "News"],
           span_
@@ -930,10 +930,10 @@ topbar =
     ]
 
 row :: [View Move] -> View Move
-row = div_ [css $ Clay.display Clay.flex <> Clay.flexDirection Clay.row]
+row = div_ [css <| Clay.display Clay.flex <> Clay.flexDirection Clay.row]
 
 column :: [View Move] -> View Move
-column = div_ [css $ Clay.display Clay.flex <> Clay.flexDirection Clay.column]
+column = div_ [css <| Clay.display Clay.flex <> Clay.flexDirection Clay.column]
 
 -- | Links
 the404 :: form -> View Move
diff --git a/Hero/Host.hs b/Hero/Host.hs
index 326738b..d547fa5 100644
--- a/Hero/Host.hs
+++ b/Hero/Host.hs
@@ -14,7 +14,7 @@
 
 -- | Hero web app
 --
--- : exe mmc
+-- : out mmc
 --
 -- : dep acid-state
 -- : dep aeson
@@ -90,10 +90,10 @@ main = bracket startup shutdown run
           keep <- Keep.open (heroKeep cfg)
           skey <- upsertKey (heroSkey cfg)
           say "hero"
-          prn $ "port: " ++ show (heroPort cfg)
-          prn $ "keep: " ++ heroKeep cfg
-          prn $ "node: " ++ heroNode cfg
-          prn $ "skey: " ++ heroSkey cfg
+          prn <| "port: " ++ show (heroPort cfg)
+          prn <| "keep: " ++ heroKeep cfg
+          prn <| "node: " ++ heroNode cfg
+          prn <| "skey: " ++ heroSkey cfg
           let jwts = Auth.defaultJWTSettings skey
               cs =
                 Auth.defaultCookieSettings
@@ -103,7 +103,7 @@ main = bracket startup shutdown run
                   }
               ctx = cs :. jwts :. EmptyContext
               proxy = Proxy @(AllRoutes '[Auth.JWT, Auth.Cookie])
-              static = serveDirectoryWith $ defaultWebAppSettings $ heroNode cfg
+              static = serveDirectoryWith <| defaultWebAppSettings <| heroNode cfg
               server =
                 -- assets, auth, and the homepage is public
                 static
@@ -200,13 +200,13 @@ wrapAuth f authResult = case authResult of
   Auth.Indefinite -> Auth.throwAll err422
 
 jsonHandlers :: AcidState Keep.HeroKeep -> User -> Server JsonApi
-jsonHandlers keep _ = Acid.query' keep $ Keep.GetComics 10
+jsonHandlers keep _ = Acid.query' keep <| Keep.GetComics 10
 
 type CssRoute = "css" :> "main.css" :> Get '[CSS] Text
 
 cssHandlers :: Server CssRoute
 cssHandlers =
-  return . Lazy.toStrict . Clay.render $ Typography.main <> Look.main
+  return . Lazy.toStrict . Clay.render <| Typography.main <> Look.main
 
 type AuthRoute =
   "auth"
@@ -241,16 +241,16 @@ authHandler ::
 authHandler cookieSettings jwtSettings loginForm =
   case loginForm of
     (LoginForm "ben@bsima.me" "test") ->
-      applyCreds $ User "ben@bsima.me" "ben" []
+      applyCreds <| User "ben@bsima.me" "ben" []
     (LoginForm "mcovino@heroprojects.io" "test") ->
-      applyCreds $ User "mcovino@heroprojects.io" "mike" []
+      applyCreds <| User "mcovino@heroprojects.io" "mike" []
     _ -> throwError err401
   where
     applyCreds usr = do
-      mApplyCookies <- liftIO $ Auth.acceptLogin cookieSettings jwtSettings usr
+      mApplyCookies <- liftIO <| Auth.acceptLogin cookieSettings jwtSettings usr
       case mApplyCookies of
         Nothing -> throwError err401
-        Just applyCookies -> return $ applyCookies usr
+        Just applyCookies -> return <| applyCookies usr
 
 -- | See also 'server' above
 type AllRoutes auths =
@@ -282,8 +282,8 @@ instance L.ToHtml a => L.ToHtml (Templated a) where
   toHtmlRaw = L.toHtml
   toHtml (Templated x) = do
     L.doctype_
-    L.html_ [L.lang_ "en"] $ do
-      L.head_ $ do
+    L.html_ [L.lang_ "en"] <| do
+      L.head_ <| do
         L.title_ "Hero [alpha]"
         L.link_ [L.rel_ "manifest", L.href_ "/manifest.json"]
         L.link_ [L.rel_ "icon", L.type_ ""]
@@ -291,7 +291,7 @@ instance L.ToHtml a => L.ToHtml (Templated a) where
         L.link_
           [ L.rel_ "apple-touch-icon",
             L.sizes_ "180x180",
-            L.href_ $
+            L.href_ <|
               Pack.cdnEdge
                 <> "/old-assets/images/favicons/apple-touch-icon.png"
           ]
@@ -299,7 +299,7 @@ instance L.ToHtml a => L.ToHtml (Templated a) where
           [ L.rel_ "icon",
             L.type_ "image/png",
             L.sizes_ "32x32",
-            L.href_ $
+            L.href_ <|
               Pack.cdnEdge
                 <> "/old-assets/images/favicons/favicon-32x32.png"
           ]
@@ -307,19 +307,19 @@ instance L.ToHtml a => L.ToHtml (Templated a) where
           [ L.rel_ "icon",
             L.type_ "image/png",
             L.sizes_ "16x16",
-            L.href_ $
+            L.href_ <|
               Pack.cdnEdge
                 <> "/old-assets/images/favicons/favicon-16x16.png"
           ]
         L.link_
           [ L.rel_ "manifest",
-            L.href_ $
+            L.href_ <|
               Pack.cdnEdge
                 <> "/old-assets/images/favicons/manifest.json"
           ]
         L.link_
           [ L.rel_ "mask-icon",
-            L.href_ $
+            L.href_ <|
               Pack.cdnEdge
                 <> "/old-assets/images/favicons/safari-pinned-tab.svg"
           ]
@@ -351,12 +351,12 @@ instance L.ToHtml a => L.ToHtml (Templated a) where
 handle404 :: Application
 handle404 _ respond =
   respond
-    $ responseLBS status404 [("Content-Type", "text/html")]
-    $ renderBS
-    $ toHtml
-    $ Templated
-    $ the404
-    $ initForm homeLink
+    <| responseLBS status404 [("Content-Type", "text/html")]
+    <| renderBS
+    <| toHtml
+    <| Templated
+    <| the404
+    <| initForm homeLink
 
 fontAwesomeRef :: MisoString
 fontAwesomeRef = "https://use.fontawesome.com/releases/v5.7.2/css/all.css"
@@ -373,30 +373,30 @@ bulmaRef =
   "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css"
 
 homeHandler :: Handler (Templated (View Move))
-homeHandler = pure . Templated . home $ initForm homeLink
+homeHandler = pure . Templated . home <| initForm homeLink
 
 comicCoverHandler :: ComicId -> Handler (Templated (View Move))
 comicCoverHandler id =
-  pure . Templated . comicCover id . initForm $ comicLink id
+  pure . Templated . comicCover id . initForm <| comicLink id
 
 comicPageHandler :: ComicId -> PageNumber -> Handler (Templated (View Move))
 comicPageHandler id n =
-  pure . Templated . comicReader id n . initForm $ comicReaderSpreadLink id n
+  pure . Templated . comicReader id n . initForm <| comicReaderSpreadLink id n
 
 comicPageFullHandler :: ComicId -> PageNumber -> Handler (Templated (View Move))
 comicPageFullHandler id n =
-  pure . Templated . comicReader id n . initForm $ comicReaderFullLink id n
+  pure . Templated . comicReader id n . initForm <| comicReaderFullLink id n
 
 comicVideoHandler :: ComicId -> PageNumber -> Handler (Templated (View Move))
 comicVideoHandler id n =
-  pure . Templated . comicReader id n . initForm $ comicVideoLink id n
+  pure . Templated . comicReader id n . initForm <| comicVideoLink id n
 
 discoverHandler :: Handler (Templated (View Move))
-discoverHandler = pure . Templated . discover $ initForm discoverLink
+discoverHandler = pure . Templated . discover <| initForm discoverLink
 
 chooseExperienceHandler :: ComicId -> PageNumber -> Handler (Templated (View Move))
 chooseExperienceHandler id n =
-  pure . Templated . comicReader id n . initForm $ chooseExperienceLink id n
+  pure . Templated . comicReader id n . initForm <| chooseExperienceLink id n
 
 loginHandler :: Handler (Templated (View Move))
-loginHandler = pure . Templated . login $ initForm loginLink
+loginHandler = pure . Templated . login <| initForm loginLink
diff --git a/Hero/Look/Typography.hs b/Hero/Look/Typography.hs
index d51cdbc..9e35ef5 100644
--- a/Hero/Look/Typography.hs
+++ b/Hero/Look/Typography.hs
@@ -52,7 +52,7 @@ fontRoot = Pack.cdnEdge <> "/old-assets/fonts/eurostile/Eurostile"
 -- | font faces
 fonts :: Css
 fonts =
-  mconcat $
+  mconcat <|
     mkEuro
       </ [ ("-Reg.otf", OpenType, fontWeight normal <> fontStyle normal),
            ("LTStd-Bold.otf", OpenType, thicc <> norm),
@@ -63,9 +63,9 @@ fonts =
          ]
   where
     mkEuro :: (Text, FontFaceFormat, Css) -> Css
-    mkEuro (sufx, fmt, extra) = fontFace $ do
+    mkEuro (sufx, fmt, extra) = fontFace <| do
       fontFamily ["Eurostile"] []
-      fontFaceSrc [FontFaceSrcUrl (fontRoot <> sufx) $ Just fmt]
+      fontFaceSrc [FontFaceSrcUrl (fontRoot <> sufx) <| Just fmt]
       extra
 
 -- TODO: add the below to Clay.Font upstream
diff --git a/Hero/Node.hs b/Hero/Node.hs
index 9934fd3..70b8217 100644
--- a/Hero/Node.hs
+++ b/Hero/Node.hs
@@ -5,7 +5,7 @@
 
 -- | Hero app frontend
 --
--- : exe mmc.js
+-- : out mmc.js
 --
 -- : dep aeson
 -- : dep clay
diff --git a/Hero/Prod.nix b/Hero/Prod.nix
index d7ab1fe..cc54f95 100644
--- a/Hero/Prod.nix
+++ b/Hero/Prod.nix
@@ -1,5 +1,16 @@
-{ config, pkgs, lib, ... }:
-{
+{ bild, lib }:
+
+# Production server for herocomics.app
+
+bild.os {
+  imports = [
+    ../Biz/OsBase.nix
+    ../Biz/Packages.nix
+    ../Biz/Users.nix
+    ./Service.nix
+  ];
+  networking.hostName = "prod-herocomics";
+  networking.domain = "herocomics.app";
   boot.loader.grub.device = "/dev/vda";
   fileSystems."/" = { device = "/dev/vda1"; fsType = "ext4"; };
   networking = {
@@ -27,8 +38,18 @@
 
     };
   };
-  services.udev.extraRules = ''
-    ATTR{address}=="b2:63:c4:e5:d6:36", NAME="eth0"
 
-  '';
+  services = {
+    herocomics = {
+      enable = true;
+      port = 3000;
+      host = bild.ghc ./Host.hs;
+      node = bild.ghcjs ./Node.hs;
+      keep = "/var/lib/hero";
+    };
+
+    udev.extraRules = ''
+      ATTR{address}=="b2:63:c4:e5:d6:36", NAME="eth0"
+    '';
+  };
 }
diff --git a/Que/Apidocs.md b/Que/Apidocs.md
new file mode 100644
index 0000000..f400889
--- /dev/null
+++ b/Que/Apidocs.md
@@ -0,0 +1,3 @@
+% que.run Api Docs
+
+coming soon
diff --git a/Que/Client.py b/Que/Client.py
new file mode 100755
index 0000000..1063eb8
--- /dev/null
+++ b/Que/Client.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+"""
+simple client for que.run
+"""
+
+import argparse
+import configparser
+import functools
+import http.client
+import logging
+import os
+import subprocess
+import sys
+import time
+import urllib.parse
+import urllib.request as request
+
+MAX_TIMEOUT = 99999999  # basically never timeout
+
+
+def auth(args):
+    "Returns the auth key for the given ns from ~/.config/que.conf"
+    logging.debug("auth")
+    namespace = args.target.split("/")[0]
+    if namespace == "pub":
+        return None
+    conf_file = os.path.expanduser("~/.config/que.conf")
+    if not os.path.exists(conf_file):
+        sys.exit("you need a ~/.config/que.conf")
+    cfg = configparser.ConfigParser()
+    cfg.read(conf_file)
+    return cfg[namespace]["key"]
+
+
+def autodecode(bytestring):
+    """Attempt to decode bytes `bs` into common codecs, preferably utf-8. If
+    no decoding is available, just return the raw bytes.
+
+    For all available codecs, see:
+    <https://docs.python.org/3/library/codecs.html#standard-encodings>
+
+    """
+    logging.debug("autodecode")
+    codecs = ["utf-8", "ascii"]
+    for codec in codecs:
+        try:
+            return bytestring.decode(codec)
+        except UnicodeDecodeError:
+            pass
+    return bytestring
+
+
+def retry(exception, tries=4, delay=3, backoff=2):
+    "Decorator for retrying an action."
+
+    def decorator(func):
+        @functools.wraps(func)
+        def func_retry(*args, **kwargs):
+            mtries, mdelay = tries, delay
+            while mtries > 1:
+                try:
+                    return func(*args, **kwargs)
+                except exception as ex:
+                    logging.debug(ex)
+                    logging.debug("retrying...")
+                    time.sleep(mdelay)
+                    mtries -= 1
+                    mdelay *= backoff
+            return func(*args, **kwargs)
+
+        return func_retry
+
+    return decorator
+
+
+def send(args):
+    "Send a message to the que."
+    logging.debug("send")
+    key = auth(args)
+    data = args.infile
+    req = request.Request(f"{args.host}/{args.target}")
+    req.add_header("User-AgenT", "Que/Client")
+    if key:
+        req.add_header("Authorization", key)
+    if args.serve:
+        logging.debug("serve")
+        while not time.sleep(1):
+            request.urlopen(req, data=data, timeout=MAX_TIMEOUT)
+
+    else:
+        request.urlopen(req, data=data, timeout=MAX_TIMEOUT)
+
+
+def then(args, msg):
+    "Perform an action when passed `--then`."
+    if args.then:
+        logging.debug("then")
+        subprocess.run(
+            args.then.format(msg=msg, que=args.target), check=False, shell=True,
+        )
+
+
+@retry(http.client.IncompleteRead, tries=10, delay=5, backoff=1)
+@retry(http.client.RemoteDisconnected, tries=10, delay=2, backoff=2)
+def recv(args):
+    "Receive a message from the que."
+    logging.debug("recv on: %s", args.target)
+    params = urllib.parse.urlencode({"poll": args.poll})
+    req = request.Request(f"{args.host}/{args.target}?{params}")
+    req.add_header("User-Agent", "Que/Client")
+    key = auth(args)
+    if key:
+        req.add_header("Authorization", key)
+    with request.urlopen(req) as _req:
+        if args.poll:
+            logging.debug("poll")
+            while not time.sleep(1):
+                logging.debug("reading")
+                msg = autodecode(_req.readline())
+                logging.debug("read")
+                print(msg, end="")
+                then(args, msg)
+        else:
+            msg = autodecode(_req.read())
+            print(msg)
+            then(args, msg)
+
+
+def get_args():
+    "Command line parser"
+    cli = argparse.ArgumentParser(description=__doc__)
+    cli.add_argument("--debug", action="store_true", help="log to stderr")
+    cli.add_argument(
+        "--host", default="http://que.run", help="where que-server is running"
+    )
+    cli.add_argument(
+        "--poll", default=False, action="store_true", help="stream data from the que"
+    )
+    cli.add_argument(
+        "--then",
+        help=" ".join(
+            [
+                "when polling, run this shell command after each response,",
+                "presumably for side effects,"
+                r"replacing '{que}' with the target and '{msg}' with the body of the response",
+            ]
+        ),
+    )
+    cli.add_argument(
+        "--serve",
+        default=False,
+        action="store_true",
+        help=" ".join(
+            [
+                "when posting to the que, do so continuously in a loop.",
+                "this can be used for serving a webpage or other file continuously",
+            ]
+        ),
+    )
+    cli.add_argument(
+        "target", help="namespace and path of the que, like 'ns/path/subpath'"
+    )
+    cli.add_argument(
+        "infile",
+        nargs="?",
+        type=argparse.FileType("rb"),
+        help="data to put on the que. Use '-' for stdin, otherwise should be a readable file",
+    )
+    return cli.parse_args()
+
+
+if __name__ == "__main__":
+    ARGV = get_args()
+    if ARGV.debug:
+        logging.basicConfig(
+            format="%(asctime)s %(message)s",
+            level=logging.DEBUG,
+            datefmt="%Y.%m.%d..%H.%M.%S",
+        )
+    try:
+        if ARGV.infile:
+            send(ARGV)
+        else:
+            recv(ARGV)
+    except KeyboardInterrupt:
+        sys.exit(0)
diff --git a/Que/Host.hs b/Que/Host.hs
index 3303709..b8e7a1a 100644
--- a/Que/Host.hs
+++ b/Que/Host.hs
@@ -11,7 +11,7 @@
 -- - <https://github.com/hargettp/courier>
 -- - sorta: <https://ngrok.com/> and <https://localtunnel.github.io/www/>
 --
--- : exe que-server
+-- : out que-server
 --
 -- : dep async
 -- : dep envy
diff --git a/Que/Index.md b/Que/Index.md
new file mode 100644
index 0000000..a9db12e
--- /dev/null
+++ b/Que/Index.md
@@ -0,0 +1,73 @@
+% que.run
+
+que.run is the concurrent, async runtime in the cloud
+
+  - runtime concurrency anywhere you have a network connection
+  - multilanguage communicating sequential processes
+  - add Go-like channels to any language
+  - connect your microservices together with the simplest possible
+    plumbing
+  - async programming as easy as running two terminal commands
+
+HTTP routes on `que.run` are Golang-like channels with a namespace and a
+path. For example: `https://que.run/pub/path/subpath`.
+
+## Quickstart
+
+There is a simple script `que` that acts as a client you can use to
+interact with the `que.run` service.
+
+Download it to somewhere on your `$PATH` and make it executable:
+
+    curl https://que.run/_/client > ~/bin/que
+    chmod +x ~/bin/que
+    que --help
+
+The client requires a recent version of Python 3.
+
+## Powerup
+
+que.run is free for limited use, but the real power of an asynchronous,
+concurrent runtime in the cloud is unlocked with some extra power-user
+features.
+
+- Free
+  - security by obscurity
+  - all protocols and data formats supported
+  - bandwidth and message sizes limited
+  - concurrent connections limited
+  - request rate limited
+- Power
+  - protect your data with private namespaces
+  - remove bandwidth and size limits
+  - private dashboard to see all of your active ques
+  - 99.999% uptime
+- Pro
+  - add durability to your ques so messages are never lost
+  - powerful batch api
+  - incredible query api
+  - Linux FUSE filesystem integration
+- Enterprise
+  - all of the Power & Pro features
+  - on-prem deployment
+  - advanced que performance monitoring
+  - SLA for support from que.run experts
+
+Email `ben@bsima.me` if you want to sign up for the Power, Pro, or
+Enterprise packages.
+
+## Quescripts
+
+We are collecting a repository of scripts that make awesome use of que:
+
+- remote desktop notifications
+- two-way communication with your phone
+- ephemeral, serverless chat rooms
+- collaborative jukebox
+
+<a id="quescripts-btn" href="/_/quescripts">See the scripts</a>
+
+## Docs
+
+- [tutorial](/_/tutorial)
+- [api docs](/_/apidocs)
diff --git a/Que/Prod.nix b/Que/Prod.nix
index 23c6f0a..b755d7c 100644
--- a/Que/Prod.nix
+++ b/Que/Prod.nix
@@ -1,5 +1,22 @@
-{ config, pkgs, lib, ... }:
-{
+{ bild, lib }:
+
+# The production server for que.run
+
+bild.os {
+  imports = [
+    ../Biz/OsBase.nix
+    ../Biz/Packages.nix
+    ../Biz/Users.nix
+    ./Host.nix
+    ./Site.nix
+  ];
+  networking.hostName = "prod-que";
+  networking.domain = "que.run";
+  services.que-server = {
+    enable = true;
+    port = 80;
+    package = bild.ghc ./Host.hs;
+  };
   boot.loader.grub.device = "/dev/vda";
   fileSystems."/" = { device = "/dev/vda1"; fsType = "ext4"; };
   swapDevices = [
@@ -30,7 +47,15 @@
       };
     };
   };
-  services.udev.extraRules = ''
-    ATTR{address}=="7a:92:a5:c6:db:c3", NAME="eth0"
-  '';
+  services = {
+    que-website = {
+      enable = true;
+      namespace = "_";
+      package = bild.ghc ./Site.hs;
+    };
+
+    udev.extraRules = ''
+      ATTR{address}=="7a:92:a5:c6:db:c3", NAME="eth0"
+    '';
+  };
 }
diff --git a/Que/Quescripts.md b/Que/Quescripts.md
new file mode 100644
index 0000000..77e7004
--- /dev/null
+++ b/Que/Quescripts.md
@@ -0,0 +1,50 @@
+% Quescripts
+
+## Remote desktop notifications
+
+Lets say we are running a job that takes a long time, maybe we are
+compiling or running a large test suite. Instead of watching the
+terminal until it completes, or flipping back to check on it every so
+often, we can create a listener that displays a popup notification when
+the job finishes.
+
+In one terminal run the listener:
+
+    que pub/notify --then "notify-send '{que}' '{msg}'"
+
+In some other terminal run the job that takes forever:
+
+    runtests ; echo "tests are done" | que pub/notify -
+
+When terminal 2 succeeds, terminal 1 will print "tests are done", then
+call the `notify-send` command, which displays a notification toast in
+Linux with title "`pub/notify`" and content "`tests are done`".
+
+Que paths are multi-producer and multi-consumer, so you can add as many
+terminals as you want.
+
+On macOS you could use something like this (just watch your quotes):
+
+    osascript -e "display notification \"{msg}\" with title \"{que}\""
+
+in place of notify-send.
+
+## Ephemeral, serverless chat rooms
+
+coming soon
+
+## Collaborative jukebox
+
+It's surprisingly easy to make a collaborative jukebox.
+
+First start up a music player:
+
+    que --poll pub/music --then "playsong '{msg}'"
+
+where `playsong` is a script that plays a file from data streaming to
+`stdin`. For example [vlc](https://www.videolan.org/vlc/) does this when
+you run it like `vlc -`.
+
+Then, anyone can submit songs with:
+
+    que pub/music song.mp3
diff --git a/Que/Site.hs b/Que/Site.hs
index 794dd04..5d2dbb8 100644
--- a/Que/Site.hs
+++ b/Que/Site.hs
@@ -5,7 +5,7 @@
 
 -- | spawns a few processes that serve the que.run website
 --
--- : exe que-website
+-- : out que-website
 --
 -- : dep async
 -- : dep config-ini
diff --git a/Que/Site.nix b/Que/Site.nix
index 685b3a6..ba2eeb2 100644
--- a/Que/Site.nix
+++ b/Que/Site.nix
@@ -5,6 +5,8 @@
 , modulesPath
 }:
 
+
+
 let
   cfg = config.services.que-website;
   static = pkgs.stdenv.mkDerivation {
diff --git a/Que/Style.css b/Que/Style.css
new file mode 100644
index 0000000..f8d1ca4
--- /dev/null
+++ b/Que/Style.css
@@ -0,0 +1,136 @@
+<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
+<style>
+:root {
+  /* base  (http://chriskempson.com/projects/base16/) */
+  --base00: #181818;
+  --base01: #282828;
+  --base02: #383838;
+  --base03: #585858;
+  --base04: #b8b8b8;
+  --base05: #d8d8d8;
+  --base06: #e8e8e8;
+  --base07: #f8f8f8;
+
+  /* highlights */
+  --base08: #ab4642;
+  --base09: #dc9656;
+  --base0A: #f7ca88;
+  --base0B: #a1b56c;
+  --base0C: #86c1b9;
+  --base0D: #7cafc2;
+  --base0E: #ba8baf;
+  --base0F: #a16946;
+}
+
+/* dark theme */
+@media ( prefers-color-scheme: dark ),
+       ( prefers-color-scheme: no-preference )
+{
+  body
+  { color: var(--base05);
+  ; background: var(--base00)
+  }
+
+  header, h1, h2, h3
+  { color: var(--base0A) }
+
+  a:link, a:visited
+  { color: var(--base0D) }
+
+  a:hover
+  { color: var(--base0C) }
+
+  pre
+  { background-color: var(--base01) }
+
+  code
+  { color: var(--base0B)
+  }
+
+  hr
+  { border: 0
+  ; height: 1px
+  ; width: 100%
+  ; margin: 2rem
+  ; background-image: linear-gradient(
+      to right,
+      /* same as --base0A */
+      rgba(186, 139, 175, 0),
+      rgba(186, 139, 175, 0.75),
+      rgba(186, 139, 175, 0))
+  }
+}
+
+/* light theme */
+
+@media ( prefers-color-scheme: light)
+{
+  body
+  { background-color: var(--base07)
+  ; color: var(--base00)
+  }
+
+  a:link, a:visited
+  { color: var(--base0D) }
+
+  a:hover
+  { color: var(--base0C) }
+
+  pre
+  { background-color: var(--base06) }
+
+  code
+  { color: var(--base0B) }
+}
+
+/* structure and layout */
+
+body
+{ max-width: 900px
+; margin: 40px auto
+; padding: 0 10px
+; font: 18px/1.5
+        "Source Sans Pro",
+        sans-serif,
+        "Apple Color Emoji",
+        "Segoe UI Emoji",
+        "Segoe UI Symbol",
+        "Noto Color Emoji"
+; display: flex
+; flex-direction: column
+; align-items: auto
+}
+
+header#title-block-header,
+h1,
+h2,
+h3
+{ line-height: 1.2
+; align-self: center
+; text-transform: lowercase
+}
+
+pre
+{ padding: .5rem }
+
+pre, code
+{ overflow-x: scroll
+; white-space: pre
+; font-family: "Source Code Pro", monospace;
+}
+
+#quescripts-btn
+{ border-width: 2px
+; border-style: solid
+}
+
+#quescripts-btn
+{ font-size: 1.2rem
+; padding: 1rem
+; text-decoration: none
+; text-align: center
+; display: block
+; max-width: 400px
+; margin: auto
+}
+</style>
diff --git a/Que/Tutorial.md b/Que/Tutorial.md
new file mode 100644
index 0000000..6542ad3
--- /dev/null
+++ b/Que/Tutorial.md
@@ -0,0 +1,53 @@
+% que.run Tutorial
+
+## Ques
+
+A que is a multi-consumer, multi-producer channel available anywhere you
+have a network connection. If you are familiar with Go channels, they
+are pretty much the same thing. Put some values in one end, and take
+them out the other end at a different time, or in a different process.
+
+Ques are created dynamically for every HTTP request you make. Here we
+use the `que` client to create a new que at the path `pub/new-que`:
+
+    que pub/new-que
+
+The `que` client is useful, but you can use anything to make the HTTP
+request, for example here's the same thing with curl:
+
+    curl https://que.run/pub/new-que
+
+These requests will block until a value is placed on the other
+end. Let's do that now. In a separate terminal:
+
+    echo "hello world" | que pub/new-que -
+
+This tells the `que` client to read the value from `stdin` and then send
+it to `example/new-que`. Or with curl:
+
+    curl https://que.run/pub/new-que -d "hello world"
+
+This will succeed immediately and send the string "`hello world`" over
+the channel, which will be received and printed by the listener in the
+other terminal.
+
+You can have as many producers and consumers attached to a channel as
+you want.
+
+## Namespaces
+
+Ques are organized into namespaces, identified by the first fragment of
+the path. In the above commands we used `pub` as the namespace, which is
+a special publically-writable namespace. The other special namespace is
+`_` which is reserved for internal use only. You can't write to the `_`
+namespace.
+
+To use other namespaces and add authentication/access controls, you can
+[sign up for the Power package](/_/index).
+
+## Events
+
+Just reading and writing data isn't very exciting, so let's throw in
+some events. We can very quickly put together a job processor.
+
+    que pub/new-que --then "./worker.sh '{msg}'"
diff --git a/Que/apidocs.md b/Que/apidocs.md
deleted file mode 100644
index f400889..0000000
--- a/Que/apidocs.md
+++ /dev/null
@@ -1,3 +0,0 @@
-% que.run Api Docs
-
-coming soon
diff --git a/Que/client.py b/Que/client.py
deleted file mode 100755
index 1063eb8..0000000
--- a/Que/client.py
+++ /dev/null
@@ -1,186 +0,0 @@
-#!/usr/bin/env python3
-"""
-simple client for que.run
-"""
-
-import argparse
-import configparser
-import functools
-import http.client
-import logging
-import os
-import subprocess
-import sys
-import time
-import urllib.parse
-import urllib.request as request
-
-MAX_TIMEOUT = 99999999  # basically never timeout
-
-
-def auth(args):
-    "Returns the auth key for the given ns from ~/.config/que.conf"
-    logging.debug("auth")
-    namespace = args.target.split("/")[0]
-    if namespace == "pub":
-        return None
-    conf_file = os.path.expanduser("~/.config/que.conf")
-    if not os.path.exists(conf_file):
-        sys.exit("you need a ~/.config/que.conf")
-    cfg = configparser.ConfigParser()
-    cfg.read(conf_file)
-    return cfg[namespace]["key"]
-
-
-def autodecode(bytestring):
-    """Attempt to decode bytes `bs` into common codecs, preferably utf-8. If
-    no decoding is available, just return the raw bytes.
-
-    For all available codecs, see:
-    <https://docs.python.org/3/library/codecs.html#standard-encodings>
-
-    """
-    logging.debug("autodecode")
-    codecs = ["utf-8", "ascii"]
-    for codec in codecs:
-        try:
-            return bytestring.decode(codec)
-        except UnicodeDecodeError:
-            pass
-    return bytestring
-
-
-def retry(exception, tries=4, delay=3, backoff=2):
-    "Decorator for retrying an action."
-
-    def decorator(func):
-        @functools.wraps(func)
-        def func_retry(*args, **kwargs):
-            mtries, mdelay = tries, delay
-            while mtries > 1:
-                try:
-                    return func(*args, **kwargs)
-                except exception as ex:
-                    logging.debug(ex)
-                    logging.debug("retrying...")
-                    time.sleep(mdelay)
-                    mtries -= 1
-                    mdelay *= backoff
-            return func(*args, **kwargs)
-
-        return func_retry
-
-    return decorator
-
-
-def send(args):
-    "Send a message to the que."
-    logging.debug("send")
-    key = auth(args)
-    data = args.infile
-    req = request.Request(f"{args.host}/{args.target}")
-    req.add_header("User-AgenT", "Que/Client")
-    if key:
-        req.add_header("Authorization", key)
-    if args.serve:
-        logging.debug("serve")
-        while not time.sleep(1):
-            request.urlopen(req, data=data, timeout=MAX_TIMEOUT)
-
-    else:
-        request.urlopen(req, data=data, timeout=MAX_TIMEOUT)
-
-
-def then(args, msg):
-    "Perform an action when passed `--then`."
-    if args.then:
-        logging.debug("then")
-        subprocess.run(
-            args.then.format(msg=msg, que=args.target), check=False, shell=True,
-        )
-
-
-@retry(http.client.IncompleteRead, tries=10, delay=5, backoff=1)
-@retry(http.client.RemoteDisconnected, tries=10, delay=2, backoff=2)
-def recv(args):
-    "Receive a message from the que."
-    logging.debug("recv on: %s", args.target)
-    params = urllib.parse.urlencode({"poll": args.poll})
-    req = request.Request(f"{args.host}/{args.target}?{params}")
-    req.add_header("User-Agent", "Que/Client")
-    key = auth(args)
-    if key:
-        req.add_header("Authorization", key)
-    with request.urlopen(req) as _req:
-        if args.poll:
-            logging.debug("poll")
-            while not time.sleep(1):
-                logging.debug("reading")
-                msg = autodecode(_req.readline())
-                logging.debug("read")
-                print(msg, end="")
-                then(args, msg)
-        else:
-            msg = autodecode(_req.read())
-            print(msg)
-            then(args, msg)
-
-
-def get_args():
-    "Command line parser"
-    cli = argparse.ArgumentParser(description=__doc__)
-    cli.add_argument("--debug", action="store_true", help="log to stderr")
-    cli.add_argument(
-        "--host", default="http://que.run", help="where que-server is running"
-    )
-    cli.add_argument(
-        "--poll", default=False, action="store_true", help="stream data from the que"
-    )
-    cli.add_argument(
-        "--then",
-        help=" ".join(
-            [
-                "when polling, run this shell command after each response,",
-                "presumably for side effects,"
-                r"replacing '{que}' with the target and '{msg}' with the body of the response",
-            ]
-        ),
-    )
-    cli.add_argument(
-        "--serve",
-        default=False,
-        action="store_true",
-        help=" ".join(
-            [
-                "when posting to the que, do so continuously in a loop.",
-                "this can be used for serving a webpage or other file continuously",
-            ]
-        ),
-    )
-    cli.add_argument(
-        "target", help="namespace and path of the que, like 'ns/path/subpath'"
-    )
-    cli.add_argument(
-        "infile",
-        nargs="?",
-        type=argparse.FileType("rb"),
-        help="data to put on the que. Use '-' for stdin, otherwise should be a readable file",
-    )
-    return cli.parse_args()
-
-
-if __name__ == "__main__":
-    ARGV = get_args()
-    if ARGV.debug:
-        logging.basicConfig(
-            format="%(asctime)s %(message)s",
-            level=logging.DEBUG,
-            datefmt="%Y.%m.%d..%H.%M.%S",
-        )
-    try:
-        if ARGV.infile:
-            send(ARGV)
-        else:
-            recv(ARGV)
-    except KeyboardInterrupt:
-        sys.exit(0)
diff --git a/Que/index.md b/Que/index.md
deleted file mode 100644
index a9db12e..0000000
--- a/Que/index.md
+++ /dev/null
@@ -1,73 +0,0 @@
-% que.run
-
-que.run is the concurrent, async runtime in the cloud
-
-  - runtime concurrency anywhere you have a network connection
-  - multilanguage communicating sequential processes
-  - add Go-like channels to any language
-  - connect your microservices together with the simplest possible
-    plumbing
-  - async programming as easy as running two terminal commands
-
-HTTP routes on `que.run` are Golang-like channels with a namespace and a
-path. For example: `https://que.run/pub/path/subpath`.
-
-## Quickstart
-
-There is a simple script `que` that acts as a client you can use to
-interact with the `que.run` service.
-
-Download it to somewhere on your `$PATH` and make it executable:
-
-    curl https://que.run/_/client > ~/bin/que
-    chmod +x ~/bin/que
-    que --help
-
-The client requires a recent version of Python 3.
-
-## Powerup
-
-que.run is free for limited use, but the real power of an asynchronous,
-concurrent runtime in the cloud is unlocked with some extra power-user
-features.
-
-- Free
-  - security by obscurity
-  - all protocols and data formats supported
-  - bandwidth and message sizes limited
-  - concurrent connections limited
-  - request rate limited
-- Power
-  - protect your data with private namespaces
-  - remove bandwidth and size limits
-  - private dashboard to see all of your active ques
-  - 99.999% uptime
-- Pro
-  - add durability to your ques so messages are never lost
-  - powerful batch api
-  - incredible query api
-  - Linux FUSE filesystem integration
-- Enterprise
-  - all of the Power & Pro features
-  - on-prem deployment
-  - advanced que performance monitoring
-  - SLA for support from que.run experts
-
-Email `ben@bsima.me` if you want to sign up for the Power, Pro, or
-Enterprise packages.
-
-## Quescripts
-
-We are collecting a repository of scripts that make awesome use of que:
-
-- remote desktop notifications
-- two-way communication with your phone
-- ephemeral, serverless chat rooms
-- collaborative jukebox
-
-<a id="quescripts-btn" href="/_/quescripts">See the scripts</a>
-
-## Docs
-
-- [tutorial](/_/tutorial)
-- [api docs](/_/apidocs)
diff --git a/Que/quescripts.md b/Que/quescripts.md
deleted file mode 100644
index 77e7004..0000000
--- a/Que/quescripts.md
+++ /dev/null
@@ -1,50 +0,0 @@
-% Quescripts
-
-## Remote desktop notifications
-
-Lets say we are running a job that takes a long time, maybe we are
-compiling or running a large test suite. Instead of watching the
-terminal until it completes, or flipping back to check on it every so
-often, we can create a listener that displays a popup notification when
-the job finishes.
-
-In one terminal run the listener:
-
-    que pub/notify --then "notify-send '{que}' '{msg}'"
-
-In some other terminal run the job that takes forever:
-
-    runtests ; echo "tests are done" | que pub/notify -
-
-When terminal 2 succeeds, terminal 1 will print "tests are done", then
-call the `notify-send` command, which displays a notification toast in
-Linux with title "`pub/notify`" and content "`tests are done`".
-
-Que paths are multi-producer and multi-consumer, so you can add as many
-terminals as you want.
-
-On macOS you could use something like this (just watch your quotes):
-
-    osascript -e "display notification \"{msg}\" with title \"{que}\""
-
-in place of notify-send.
-
-## Ephemeral, serverless chat rooms
-
-coming soon
-
-## Collaborative jukebox
-
-It's surprisingly easy to make a collaborative jukebox.
-
-First start up a music player:
-
-    que --poll pub/music --then "playsong '{msg}'"
-
-where `playsong` is a script that plays a file from data streaming to
-`stdin`. For example [vlc](https://www.videolan.org/vlc/) does this when
-you run it like `vlc -`.
-
-Then, anyone can submit songs with:
-
-    que pub/music song.mp3
diff --git a/Que/style.css b/Que/style.css
deleted file mode 100644
index f8d1ca4..0000000
--- a/Que/style.css
+++ /dev/null
@@ -1,136 +0,0 @@
-<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
-<style>
-:root {
-  /* base  (http://chriskempson.com/projects/base16/) */
-  --base00: #181818;
-  --base01: #282828;
-  --base02: #383838;
-  --base03: #585858;
-  --base04: #b8b8b8;
-  --base05: #d8d8d8;
-  --base06: #e8e8e8;
-  --base07: #f8f8f8;
-
-  /* highlights */
-  --base08: #ab4642;
-  --base09: #dc9656;
-  --base0A: #f7ca88;
-  --base0B: #a1b56c;
-  --base0C: #86c1b9;
-  --base0D: #7cafc2;
-  --base0E: #ba8baf;
-  --base0F: #a16946;
-}
-
-/* dark theme */
-@media ( prefers-color-scheme: dark ),
-       ( prefers-color-scheme: no-preference )
-{
-  body
-  { color: var(--base05);
-  ; background: var(--base00)
-  }
-
-  header, h1, h2, h3
-  { color: var(--base0A) }
-
-  a:link, a:visited
-  { color: var(--base0D) }
-
-  a:hover
-  { color: var(--base0C) }
-
-  pre
-  { background-color: var(--base01) }
-
-  code
-  { color: var(--base0B)
-  }
-
-  hr
-  { border: 0
-  ; height: 1px
-  ; width: 100%
-  ; margin: 2rem
-  ; background-image: linear-gradient(
-      to right,
-      /* same as --base0A */
-      rgba(186, 139, 175, 0),
-      rgba(186, 139, 175, 0.75),
-      rgba(186, 139, 175, 0))
-  }
-}
-
-/* light theme */
-
-@media ( prefers-color-scheme: light)
-{
-  body
-  { background-color: var(--base07)
-  ; color: var(--base00)
-  }
-
-  a:link, a:visited
-  { color: var(--base0D) }
-
-  a:hover
-  { color: var(--base0C) }
-
-  pre
-  { background-color: var(--base06) }
-
-  code
-  { color: var(--base0B) }
-}
-
-/* structure and layout */
-
-body
-{ max-width: 900px
-; margin: 40px auto
-; padding: 0 10px
-; font: 18px/1.5
-        "Source Sans Pro",
-        sans-serif,
-        "Apple Color Emoji",
-        "Segoe UI Emoji",
-        "Segoe UI Symbol",
-        "Noto Color Emoji"
-; display: flex
-; flex-direction: column
-; align-items: auto
-}
-
-header#title-block-header,
-h1,
-h2,
-h3
-{ line-height: 1.2
-; align-self: center
-; text-transform: lowercase
-}
-
-pre
-{ padding: .5rem }
-
-pre, code
-{ overflow-x: scroll
-; white-space: pre
-; font-family: "Source Code Pro", monospace;
-}
-
-#quescripts-btn
-{ border-width: 2px
-; border-style: solid
-}
-
-#quescripts-btn
-{ font-size: 1.2rem
-; padding: 1rem
-; text-decoration: none
-; text-align: center
-; display: block
-; max-width: 400px
-; margin: auto
-}
-</style>
diff --git a/Que/tutorial.md b/Que/tutorial.md
deleted file mode 100644
index 6542ad3..0000000
--- a/Que/tutorial.md
+++ /dev/null
@@ -1,53 +0,0 @@
-% que.run Tutorial
-
-## Ques
-
-A que is a multi-consumer, multi-producer channel available anywhere you
-have a network connection. If you are familiar with Go channels, they
-are pretty much the same thing. Put some values in one end, and take
-them out the other end at a different time, or in a different process.
-
-Ques are created dynamically for every HTTP request you make. Here we
-use the `que` client to create a new que at the path `pub/new-que`:
-
-    que pub/new-que
-
-The `que` client is useful, but you can use anything to make the HTTP
-request, for example here's the same thing with curl:
-
-    curl https://que.run/pub/new-que
-
-These requests will block until a value is placed on the other
-end. Let's do that now. In a separate terminal:
-
-    echo "hello world" | que pub/new-que -
-
-This tells the `que` client to read the value from `stdin` and then send
-it to `example/new-que`. Or with curl:
-
-    curl https://que.run/pub/new-que -d "hello world"
-
-This will succeed immediately and send the string "`hello world`" over
-the channel, which will be received and printed by the listener in the
-other terminal.
-
-You can have as many producers and consumers attached to a channel as
-you want.
-
-## Namespaces
-
-Ques are organized into namespaces, identified by the first fragment of
-the path. In the above commands we used `pub` as the namespace, which is
-a special publically-writable namespace. The other special namespace is
-`_` which is reserved for internal use only. You can't write to the `_`
-namespace.
-
-To use other namespaces and add authentication/access controls, you can
-[sign up for the Power package](/_/index).
-
-## Events
-
-Just reading and writing data isn't very exciting, so let's throw in
-some events. We can very quickly put together a job processor.
-
-    que pub/new-que --then "./worker.sh '{msg}'"
diff --git a/default.nix b/default.nix
deleted file mode 100644
index 882ffa5..0000000
--- a/default.nix
+++ /dev/null
@@ -1,97 +0,0 @@
-let
-  nixpkgs = import ./Biz/Bild/Nixpkgs.nix;
-  build = import ./Biz/Bild/Rules.nix { inherit nixpkgs; };
-  nixos-mailserver = let ver = "v2.3.0"; in builtins.fetchTarball {
-    url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${ver}/nixos-mailserver-${ver}.tar.gz";
-    sha256 = "0lpz08qviccvpfws2nm83n7m2r8add2wvfg9bljx9yxx8107r919";
-  };
-in rec {
-  # Cloud infrastructure, always online. Mostly for messaging-related
-  # stuff.
-  #
-  Biz.Cloud = build.os {
-    imports = [
-      ./Biz/OsBase.nix
-      ./Biz/Packages.nix
-      ./Biz/Users.nix
-      ./Biz/Cloud/Chat.nix
-      ./Biz/Cloud/Git.nix
-      ./Biz/Cloud/Hardware.nix
-      ./Biz/Cloud/Mail.nix
-      ./Biz/Cloud/Networking.nix
-      ./Biz/Cloud/Web.nix
-      ./Biz/Cloud/Znc.nix
-      nixos-mailserver
-    ];
-    networking.hostName = "simatime";
-    networking.domain = "simatime.com";
-  };
-  # Dev machine for work and building stuff.
-  #
-  Biz.Dev = build.os {
-    imports = [
-      ./Biz/OsBase.nix
-      ./Biz/Packages.nix
-      ./Biz/Users.nix
-      ./Biz/Dev/Configuration.nix
-      ./Biz/Dev/Hardware.nix
-    ];
-    networking.hostName = "lithium";
-    networking.domain = "dev.simatime.com";
-  };
-  # The production server for que.run
-  #
-  Que.Prod = build.os {
-    imports = [
-      ./Biz/OsBase.nix
-      ./Biz/Packages.nix
-      ./Biz/Users.nix
-      ./Que/Host.nix
-      ./Que/Site.nix
-      ./Que/Prod.nix
-    ];
-    networking.hostName = "prod-que";
-    networking.domain = "que.run";
-    services.que-server = {
-      enable = true;
-      port = 80;
-      package = Que.Host;
-    };
-    services.que-website = {
-      enable = true;
-      namespace = "_";
-      package = Que.Site;
-    };
-  };
-  # Production server for herocomics.app
-  Hero.Prod = build.os {
-    imports = [
-      ./Biz/OsBase.nix
-      ./Biz/Packages.nix
-      ./Biz/Users.nix
-      ./Hero/Service.nix
-      ./Hero/Prod.nix
-    ];
-    networking.hostName = "prod-herocomics";
-    networking.domain = "herocomics.app";
-    services.herocomics = {
-      enable = true;
-      port = 3000;
-      host = Hero.Host;
-      node = Hero.Node;
-      keep = "/var/lib/hero";
-    };
-  };
-  # Haskell targets
-  #
-  Biz.Ibb.Server = build.ghc Biz/Ibb/Server.hs;
-  Biz.Ibb.Client = build.ghcjs Biz/Ibb/Client.hs;
-  Hero.Host = build.ghc Hero/Host.hs;
-  Hero.Node =  build.ghcjs Hero/Node.hs;
-  Que.Host = build.ghc ./Que/Host.hs;
-  Que.Site = build.ghc ./Que/Site.hs;
-  # Development environment
-  env = build.env;
-  # Fall through to any of our overlay packages
-  inherit nixpkgs;
-}
diff --git a/shell.nix b/shell.nix
index 7e64e11..c51f5b8 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1 +1,3 @@
-(import ./default.nix).env
+(import ./Biz/Bild/Rules.nix {
+  nixpkgs = import ./Biz/Bild/Nixpkgs.nix;
+}).env
-- 
cgit v1.2.3