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