diff --git a/index.org b/index.org
index 92ad1c3..07fe7f8 100644
--- a/index.org
+++ b/index.org
@@ -14,4 +14,3 @@
* [[file:multi-room-audio.org][Multi-room audio setup]]
* [[file:vi-everywhere.org][vi-editing everywhere]]
* [[file:aws-metric-filters.org][AWS Cloudwatch Metric Filters]]
-
diff --git a/one.org b/one.org
new file mode 100644
index 0000000..ed3a967
--- /dev/null
+++ b/one.org
@@ -0,0 +1,440 @@
+* circumlocuting
+:PROPERTIES:
+:ONE: wfot-default-home-list-pages
+:CUSTOM_ID: /
+:END:
+
+Hello,
+
+Here's what I've been thinking about
+
+* The problem with large companies
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/large-companies/
+:END:
+
+Organizing people is a difficult problem which only gets more difficult as youmore people need to be organized.
+
+The larger a company is the more of its internal structures, rules, policies, history, etc are devoted _just_ to organizing people.
+
+For me, realizing this was like the first time you hear a flourescent light buzzing in an otherwise quiet room.
+
+Reasonable people can differ on this point, but for my own sake I'd much rather avoid all the people-organizing baggage that comes with large companies.
+
+I don't have a hard-and-fast rule about the size of a place I want to work but the larger a place is then generally the more reason I need to want to be there.
+
+Of course, this is all kind of theoretical at this point, as [[https://flipstone.com][Flipstone]] is my forever home.
+
+#+BEGIN_SRC haskell
+
+doThings :: String -> T.Text -> NEL.NonEmpty Char -> BS.ByteString -> IO ()
+doThings = error "WTF is this"
+
+#+END_SRC
+
+* Simple CSS frameworks - 2024-09-30
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/simple-css-frameworks/
+:END:
+
+I really like simple drop-in CSS resets like the one I use for this site.
+
+At the time of writing, I'm using [[https://picocss.com/][Pico]] but I also considered [[https://yegor256.github.io/tacit/][tacit]]
+
+The idea is that they provide nice default styling of HTML elements out of the box without the need to reference any specific classes.
+
+The idea works well for sites that are much more content than layout - like this one.
+
+Using tacit is a matter of incluing this link tag in the page's HEAD element:
+
+#+BEGIN_SRC html
+
+#+END_SRC
+* Let people fail - 2024-09-25
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/let-people-fail/
+:END:
+
+** How (and why) to let people fail
+Warning: This, like most things, will involve a fair bit of projection.
+
+Effective and enjoyable collaboration with other people requires mutual trust.
+
+I believe that for someone to feel trusted by another person then they need the space to fail.
+
+I _think_ this is obvious when considering what not having the space to fail looks like.
+
+Not having the space to fail means your collaborator is doing one of two things:
+
+1. Directing every action you take a.k.a. micromanaging
+2. Coming behind you and redoing all of your work
+
+Both of these are attempts by the other person to minimize risk (or simply cases where they're failing to manage their own anxieties).
+
+These actions are counter productive to fostering trust and should be avoided unless failure is too costly.
+
+I'm _not_ saying all collaboration _requires_ building trust. There are times when you simply can't afford failure or mistakes.
+
+What I am saying is that people frequently misjudge the value in deliberately giving others the space to fail for the sake of fostering trust.
+
+Building trust is important and we should do it deliberately.
+* TODO Just what is it you do here?
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/job-description/
+:END:
+
+I've never liked working at [[#/blog/large-companies/][large companies]]. Mostly because I think
+they complicate things, but some things are more complicated at small
+companies.
+
+* TODO Managing Expectations
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/managing-expectations/
+:END:
+
+I'll figure this out one day. Until then I'll just keep saying yes and burning myself out making everyone happy.
+
+* HTTPS @ Homelab
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/large-companies/
+:END:
+** HTTPS @ Home
+I run a lot of services at home.
+
+This includes, but isn't limited to
+
+- [[https://archivebox.io/][ArchiveBox]]
+- [[https://github.com/dani-garcia/vaultwarden][VaultWarden]]
+- [[https://github.com/navidrome/navidrome][Navidrome]]
+- [[https://plex.tv][Plex]]
+- [[https://github.com/LibrePhotos/librephotos][LibrePhotos]]
+- This blog
+
+and a lot more.
+
+Pretty much anything that's served up over HTTP is always nice if not
+necessary to have behind TLS.
+
+[[https://letsencrypt.org/][LetsEncrypt]] long ago brought free certs to
+the masses and there are a lot of tools for automating that nowadays.
+
+My preferred approach for getting all the unnecessary nonsense I
+self-host at home behind TLS is [[https://caddyserver.com][Caddy]].
+
+I have a super straight forward setup, generally:
+
+- Run Caddy in a docker container
+- Create a wildcard CNAME record in my DNS pointing at my home's
+ (effectively) static IP
+- Add an entry in my Caddyfile for each services I'm running at home on
+ its own subdomain
+- If it's a service then I add it with a =reverse_proxy= block
+- If it's a static site (like this) then there's a block for
+- If it's something I want only accessible on my home network then I put
+ a block like
+
+#+BEGIN_EXAMPLE
+ @local_network {
+ path *
+ remote_ip
+ }
+#+END_EXAMPLE
+
+in the directive. And voila.
+
+Then tell Caddy to reload the config and I'm done.
+* Multi-room audio setup
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/multi-room-audio/
+:END:
+** My multiroom audio setup
+
+I've put my home audio solution together out of the following
+components.
+
+- [[https://github.com/badaix/snapcast][Snapcast]]
+
+- [[https://www.musicpd.org/][MPD]]
+
+- [[https://github.com/librespot-org/librespot][Librespot]]
+
+- [[https://github.com/mikebrady/shairport-sync][Shairport-sync]]
+
+- A mini-PC in my closet running the above software
+
+- Two Raspberry Pi 4s
+
+- Four Raspberry Pi Zero Ws
+
+- Some desktop speakers and some Bluetooth speakers (wired to the Pis)
+
+Each of the Raspberry Pis is in a room or porch attached to a speaker.
+
+Snapcast lets me take an audio source and synchronize it across multiple
+clients. Each of the Raspberry Pis are running a =snapclient= instance
+and play whatever the =snapserver= instance tells them to.
+
+Snapcast is setup to send whichever of the streams (MPD, Spotify,
+Shairport-sync/AirPlay) is playing audio to each of the clients that are
+connected to it.
+
+This lets me or anyone else on my WiFi network play directly on one or
+more of the speakers - each named for the room that they're in using
+either Spotify, AirPlay, picking from my own music collection or by
+pointing at a URL (like to a podcast episode).
+
+This works out great and we've used it at home for the past year.
+
+I'd like to get the podcast experience to a more seamless place but it's
+pretty OK right now using AirMusic on my phone to play audio to the
+speakers over AirPlay.
+
+* vi-editing everywhere
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/vi-everywhere/
+:END:
+
+** vi modal editing in most places
+For my sake, I prefer to have Vim bindings in as many places as
+possible.
+
+Most shells can be configured to use Vim bindings by putting =set -o vi=
+somewhere in your shell startup script.
+
+If you're using ZSH then you'll probably want an additional binding to
+restore CTRL-R reverse history search.
+
+=bindkey '^R' history-incremental-search-backward=
+
+For CLI tools that use the =readline= library then you can configure its
+input mode using a =.inputrc= file in your =$HOME= directory.
+
+This affects REPLs like =ghci= and tools like =psql=.
+
+#+begin_src txt
+set editing-mode vi
+$if mode=vi
+
+set keymap vi-command
+# these are for vi-command mode
+Control-l: clear-screen
+
+set keymap vi-insert
+# these are for vi-insert mode
+Control-l: clear-screen
+$endif
+#+end_src
+* AWS Cloudwatch Metric Filters
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/large-companies/
+:END:
+
+** Structed and passively collected metrics via AWS CloudWatch
+
+AWS is a vast and sprawling set of services. It can be hard to find the
+hidden gems like this one so I wanted to point this one out.
+
+Structured metrics are very helpful to monitoring the health and
+function of an software system.
+
+- Do you want to know how long a particular transaction typically takes?
+- How fast your database queries are?
+- How long external APIs take to respond?
+- Fire an alert when a particular function on the site happens too many
+ times? Or too few times?
+
+...plus a million other things specific to whatever system you're
+working on.
+
+There are a lot of great tools for doing this and one that you might not
+know about is AWS CloudWatch Metric Filters. If you're already on AWS
+then you should consider these because it requires only that your
+application logs to CloudWatch.
+
+If you're on ECS then the [[https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_awslogs.html][awslogs]] log driver for Docker gets you that
+nearly for free. By "free" I mean that your application itself can
+have /zero/ dependencies on AWS services and not require any AWS
+credentials or libraries to start pumping out metrics that you can
+visualize, alert on and record over time.
+
+The [[https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/MonitoringLogData.html][AWS docs]] themselves offer the canonical reference for configuring
+these so I won't go into detail here.
+
+However, the gist is that for a log filter you define the following
+properties
+
+- A filter pattern for extracting a discrete metric value out of a log
+ entry
+- A metric name to store the value in
+- An optional dimension for sub-classifying the value
+- And finally a log group to extract the metric values from
+
+After that you just run the application and as the logs roll in the
+metric values get pumped out. Then you can [[https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Create-alarm-on-metric-math-expression.html][define alarms for alerting]]
+on them, [[https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Dashboards.html][graph them]], [[https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-scaling-simple-step.html#policy-creating-alarm-console][define autoscaling rules]] from them and more.
+
+To conclude - AWS is big and hairy. While there are benefits to staying
+platform agnostic, some AWS services don't require much or any coupling
+of your application code to take advantage of. Cloudwatch Metrics is one
+of those services and you can get a lot of value out of it with not much
+effort.
+
+
+* The default page
+:PROPERTIES:
+:ONE: wfot-default
+:CUSTOM_ID: /blog/default/
+:END:
+
+This page is rendered with the default render function ~one-default~
+specified in ~ONE~ org property.
+
+** Do you want a table of content?
+
+As you can see, ~one-default~ doesn't add a table of content (TOC). If
+you want a default render function that adds the TOC to the page you can
+use the render function ~one-default-with-toc~ presented in [[#/blog/one-default-with-toc/][The default
+page with a TOC]].
+
+** Headline foo
+*** Headline bar
+
+Some content.
+
+*** Headline baz
+
+#+BEGIN_SRC bash :results verbatim
+tree
+#+END_SRC
+
+#+RESULTS:
+#+begin_example
+.
+├── assets
+│ └── one.css
+├── one.org
+└── public
+ ├── blog
+ │ ├── default
+ │ │ └── index.html
+ │ ├── default-home-list-pages
+ │ │ └── index.html
+ │ ├── one-default-doc
+ │ │ └── index.html
+ │ ├── one-default-with-sidebar
+ │ │ └── index.html
+ │ └── one-default-with-toc
+ │ └── index.html
+ ├── index.html
+ └── one.css
+
+8 directories, 9 files
+#+end_example
+
+* The default page with a sidebar
+:PROPERTIES:
+:ONE: wfot-default-with-sidebar
+:CUSTOM_ID: /blog/one-default-with-sidebar/
+:END:
+
+This page is rendered with the render function ~one-default-with-sidebar~
+specified in the org property ~ONE~.
+
+** Do you want a sidebar and a TOC?
+
+Perhaps you want a sidebar listing all the pages on your website and a
+table of content, as many modern documentation sites do. If so, you
+can use the default render function ~one-default-doc~ presented in [[#/blog/one-default-doc/][The
+default page with TOC and sidebar]].
+
+** Headline foo
+*** Headline bar
+
+Some content.
+
+*** Headline baz
+
+#+BEGIN_SRC bash :results verbatim
+tree
+#+END_SRC
+
+#+RESULTS:
+#+begin_example
+.
+├── assets
+│ └── one.css
+├── one.org
+└── public
+ ├── blog
+ │ ├── default
+ │ │ └── index.html
+ │ ├── default-home-list-pages
+ │ │ └── index.html
+ │ ├── one-default-doc
+ │ │ └── index.html
+ │ ├── one-default-with-sidebar
+ │ │ └── index.html
+ │ └── one-default-with-toc
+ │ └── index.html
+ ├── index.html
+ └── one.css
+
+8 directories, 9 files
+#+end_example
+
+* The default page with TOC and sidebar
+:PROPERTIES:
+:ONE: wfot-default-doc
+:CUSTOM_ID: /blog/one-default-doc/
+:END:
+
+This page is rendered with the function ~one-default-doc~ specified
+in the org property ~ONE~.
+
+** Do you want to know more about one.el?
+
+Check the documentation at https://one.tonyaldon.com.
+
+** Headline foo
+*** Headline bar
+
+Some content.
+
+*** Headline baz
+
+#+BEGIN_SRC bash :results verbatim
+tree
+#+END_SRC
+
+#+RESULTS:
+#+begin_example
+.
+├── assets
+│ └── one.css
+├── one.org
+└── public
+ ├── blog
+ │ ├── default
+ │ │ └── index.html
+ │ ├── default-home-list-pages
+ │ │ └── index.html
+ │ ├── one-default-doc
+ │ │ └── index.html
+ │ ├── one-default-with-sidebar
+ │ │ └── index.html
+ │ └── one-default-with-toc
+ │ └── index.html
+ ├── index.html
+ └── one.css
+
+8 directories, 9 files
+#+end_example
diff --git a/onerc.el b/onerc.el
new file mode 100644
index 0000000..8270805
--- /dev/null
+++ b/onerc.el
@@ -0,0 +1,160 @@
+(setq wfot-css "https://cdn.jsdelivr.net/npm/holiday.css@0.11.2")
+
+(defun wfot-default (page-tree pages _global)
+ "willfullyobtuse default render function
+
+ See `one-is-page', `one-render-pages' and `one-default-css'."
+ (let* ((title (org-element-property :raw-value page-tree))
+ (path (org-element-property :CUSTOM_ID page-tree))
+ (content (org-export-data-with-backend
+ (org-element-contents page-tree)
+ 'one-ox nil))
+ (website-name (one-default-website-name pages))
+ (nav (one-default-nav path pages)))
+ (jack-html
+ ""
+ `(:html
+ (:head
+ (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
+ (:link (@ :rel "stylesheet" :href wfot-css))
+ (:title ,title))
+ (:body
+ (:div.header (:a (@ :href "/") ,website-name))
+ (:div.content
+ (:div.title
+ ,(if (not (string= path "/"))
+ `(:div.title (:h1 ,title))
+ '(:div.title-empty)))
+ ,content
+ ,nav))))))
+
+(defun wfot-default-home-list-pages (page-tree pages _global)
+ "Default render function to use in the home page that lists pages.
+
+See `one-is-page' for the meaning of PAGE-TREE and PAGES.
+
+Also see `one-render-pages' and `one-default-css'."
+ (let* ((title (org-element-property :raw-value page-tree))
+ (content (org-export-data-with-backend
+ (org-element-contents page-tree)
+ 'one-ox nil))
+ (website-name (one-default-website-name pages))
+ ;; All pages but the home pages
+ (pages-list (one-default-pages pages "/.+")))
+ (jack-html
+ ""
+ `(:html
+ (:head
+ (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
+ (:link (@ :rel "stylesheet" :type "text/css" :href wfot-css))
+ (:title ,title))
+ (:body
+ (:div.header (:a (@ :href "/") ,website-name))
+ (:div.content
+ (:div/home-list-pages ,content)
+ (:div/pages (:ul ,(reverse pages-list)))))))))
+
+(defun wfot-default-home (page-tree pages _global)
+ "Default render function to use in the home page.
+
+See `one-is-page' for the meaning of PAGE-TREE and PAGES.
+
+Also see `one-render-pages' and `one-default-css'."
+ (let* ((title (org-element-property :raw-value page-tree))
+ (content (org-export-data-with-backend
+ (org-element-contents page-tree)
+ 'one-ox nil))
+ (website-name (one-default-website-name pages)))
+ (jack-html
+ ""
+ `(:html
+ (:head
+ (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
+ (:link (@ :rel "stylesheet" :type "text/css" :href wfot-css))
+ (:title ,title))
+ (:body
+ (:div.header ,website-name)
+ (:div.content
+ (:div/home ,content)))))))
+
+(defun wfot-default-with-sidebar (page-tree pages global)
+ "Default render function with a sidebar listing PAGES.
+
+See `one-is-page' for the meaning of PAGE-TREE and GLOBAL.
+
+Also see `one-default-sidebar', `one-render-pages' and `one-default-css'."
+ (wfot-default-sidebar page-tree pages global))
+
+(defun wfot-default-sidebar (page-tree pages _global &optional with-toc)
+ "Return a HTML string with PAGES listed in a sidebar.
+
+The arguments PAGE-TREE, PAGES and _GLOBAL are the same as
+render functions take (See `one-is-page').
+
+When WITH-TOC is non-nil, add the table of content of PAGE-TREE
+in the HTML string.
+
+This function is meant to be used by `one-default-with-sidebar'
+and `one-default-doc' render functions.
+
+See `one-render-pages', `one-default-css' and `one-default-pages'."
+ (let* ((title (org-element-property :raw-value page-tree))
+ (path (org-element-property :CUSTOM_ID page-tree))
+ (content (org-export-data-with-backend
+ (org-element-contents page-tree)
+ 'one-ox nil))
+ (website-name (one-default-website-name pages))
+ (pages-list (one-default-pages pages))
+ (headlines (cdr (one-default-list-headlines page-tree)))
+ (toc (one-default-toc-component headlines))
+ (nav (one-default-nav path pages)))
+ (jack-html
+ ""
+ `(:html
+ (:head
+ (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
+ (:link (@ :rel "stylesheet" :type "text/css" :href wfot-css))
+ (:title ,title))
+ (:body
+ ;; sidebar-left and sidebar-main are for small devices
+ (:div/sidebar-left (@ :onclick "followSidebarLink()")
+ (:div (:div "Pages"))
+ ,pages-list)
+ (:div/sidebar-main)
+ (:div/sidebar-header
+ (:svg/hamburger (@ :viewBox "0 0 24 24" :onclick "sidebarShow()")
+ (:path (@ :d "M21,6H3V5h18V6z M21,11H3v1h18V11z M21,17H3v1h18V17z")))
+ (:a (@ :href "/") ,website-name))
+ (:div/sidebar-content
+ (:div/sidebar ,pages-list)
+ (:article
+ ,(if (not (string= path "/"))
+ `(:div.title (:h1 ,title))
+ '(:div.title-empty))
+ ,(when with-toc toc)
+ ,content
+ ,nav)))
+ (:script "
+function sidebarShow() {
+ if (window.innerWidth < 481)
+ document.getElementById('sidebar-left').style.width = '75vw';
+ else {
+ document.getElementById('sidebar-left').style.width = 'min(300px, 34vw)';
+ }
+ document.getElementById('sidebar-main').setAttribute('onclick', 'sidebarHide()');
+ document.getElementById('sidebar-main').style.display = 'block';
+}
+function sidebarHide() {
+ document.getElementById('sidebar-left').style.width = '0';
+ document.getElementById('sidebar-main').style.display = 'none';
+}
+")))))
+
+
+(defun wfot-default-doc (page-tree pages global)
+ "Default render function with a sidebar listing PAGES and the table of content.
+
+See `one-is-page' for the meaning of PAGE-TREE and GLOBAL.
+
+Also see `one-default-sidebar', `one-render-pages' and `one-default-css'."
+ (wfot-default-sidebar page-tree pages global 'with-toc))
diff --git a/public/.gitkeep b/public/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/setup.org b/setup.org
index 14dbcbd..6933e8b 100644
--- a/setup.org
+++ b/setup.org
@@ -8,8 +8,8 @@
#+html_head_simple:
#+html_head_holiday:
#+html_head_mvp:
-#+html_head_pico:
-#+html_head:
+#+html_head:
+#+html_head_pico_amber:
#+html_head_tacit:
#+html_container: main
#+html_content_class: container
diff --git a/vi-everywhere.org b/vi-everywhere.org
index 62d9d4d..f082820 100644
--- a/vi-everywhere.org
+++ b/vi-everywhere.org
@@ -4,8 +4,6 @@
#+subtitle:
:END:
** vi modal editing in most places
-#+HTML:
-#+HTML:
For my sake, I prefer to have Vim bindings in as many places as
possible.
@@ -21,8 +19,6 @@ For CLI tools that use the =readline= library then you can configure its
input mode using a =.inputrc= file in your =$HOME= directory.
This affects REPLs like =ghci= and tools like =psql=.
-#+HTML:
-#+HTML:
#+begin_src txt
set editing-mode vi
@@ -37,87 +33,3 @@ set keymap vi-insert
Control-l: clear-screen
$endif
#+end_src
-
-
-#+BEGIN_SRC haskell
-module Data.Validation.Aeson where
-
-import Control.Monad.Identity
-
-import Data.Aeson
-import qualified Data.Aeson.Key as Key
-import qualified Data.Aeson.KeyMap as KeyMap
-import qualified Data.ByteString as BS
-import qualified Data.ByteString.Lazy as LazyBS
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-import qualified Data.Vector as Vec
-
-import Data.Validation.Types
-
-decodeValidJSON :: Validator Value a -> LazyBS.ByteString -> ValidationResult a
-decodeValidJSON validator input =
- runIdentity (decodeValidJSONT (liftV validator) input)
-
-decodeValidJSONStrict :: Validator Value a -> BS.ByteString -> ValidationResult a
-decodeValidJSONStrict validator input =
- runIdentity (decodeValidJSONStrictT (liftV validator) input)
-
-decodeValidJSONT ::
- Applicative m =>
- ValidatorT Value m a ->
- LazyBS.ByteString ->
- m (ValidationResult a)
-decodeValidJSONT validator input =
- case eitherDecode input of
- Left err -> pure $ Invalid (errMessage $ Text.pack err)
- Right value -> runValidatorT validator (value :: Value)
-
-decodeValidJSONStrictT ::
- Applicative m =>
- ValidatorT Value m a ->
- BS.ByteString ->
- m (ValidationResult a)
-decodeValidJSONStrictT validator input =
- case eitherDecodeStrict input of
- Left err -> pure $ Invalid (errMessage $ Text.pack err)
- Right value -> runValidatorT validator (value :: Value)
-
-instance Validatable Value where
- inputText (String text) = Just text
- inputText _ = Nothing
-
- inputNull Null = IsNull
- inputNull _ = NotNull
-
- inputBool (Bool True) = Just True
- inputBool (Bool False) = Just False
- inputBool _ = Nothing
-
- arrayItems (Array items) = Just items
- arrayItems _ = Nothing
-
- scientificNumber (Number sci) = Just sci
- scientificNumber _ = Nothing
-
- lookupChild attrName (Object hmap) =
- LookupResult $
- KeyMap.lookup (Key.fromText attrName) hmap
- lookupChild _ _ = InvalidLookup
-
-instance ToJSON Errors where
- toJSON (Messages set) =
- Array
- . Vec.fromList
- . map toJSON
- . Set.toList
- $ set
- toJSON (Group attrs) =
- Object
- . KeyMap.fromList
- . Map.toList
- . Map.mapKeys Key.fromText
- . Map.map toJSON
- $ attrs
-#+END_SRC