Compare commits

...

146 Commits

Author SHA1 Message Date
Neale Pickett fad7d10e60 Add normal-size images
Homepage / publish (push) Successful in 9s Details
2023-12-24 15:20:16 -07:00
Neale Pickett 00609d2261 My generation blag post 2023-12-24 14:42:37 -07:00
Neale Pickett 7fbeb6fe46 more color on links 2023-12-24 14:40:22 -07:00
Neale Pickett 9300e3964a move to smaller jpg 2023-12-24 13:27:31 -07:00
Neale Pickett 5d7cc9ca1a contrast ratio 2023-12-24 13:23:24 -07:00
Neale Pickett fb91535dda fix xmas eve salad date 2023-12-24 13:16:46 -07:00
Neale Pickett f1493024ea Finally do dark mode properly 2023-12-24 13:16:08 -07:00
Neale Pickett 7c0699b583 Ginnie blog: no tags 2023-12-24 12:51:37 -07:00
Neale Pickett 1eccdaf317 Adding dates, subtitles to Ginnie's Salads 2023-12-24 12:47:28 -07:00
Neale Pickett a69a8c068e Added Ginnie's Salads Section 2023-12-24 11:54:07 -07:00
Neale Pickett 748dd223a0 Train bag 2023-12-24 10:32:55 -07:00
Neale Pickett c08f76d759 Clarify wording 2023-12-22 14:27:21 -07:00
Neale Pickett de1454dfd5 Clever design, not simplicity 2023-12-22 14:24:18 -07:00
Neale Pickett 42289af1d9 Merge branch 'main' of https://git.woozle.org/neale/homepage 2023-12-22 14:21:17 -07:00
Neale Pickett 5fb075805d Alpine linux homelab 2023-12-22 14:21:02 -07:00
Neale Pickett 73a54c086b Windows blog entry 2023-12-15 12:49:30 -07:00
Neale Pickett 09cc88930e tpyo 2023-11-29 17:56:42 -07:00
Neale Pickett 4fed030521 correctly fudge date 2023-11-29 17:55:40 -07:00
Neale Pickett f8da2e386a Big Builder blog post 2023-11-29 17:43:38 -07:00
Neale Pickett 9b97b2b14c Merge branch 'main' of https://git.woozle.org/neale/homepage 2023-11-29 17:37:54 -07:00
Neale Pickett 1bfabcf9cb self-hosted email blog post 2023-11-29 17:37:47 -07:00
Neale Pickett 9f41915514 Some edits 2023-10-28 12:49:06 -07:00
Neale Pickett db84e299da Your first train trip 2023-10-28 10:56:31 -07:00
Neale Pickett 57d44bdf2a CI/CD 2023-10-27 21:39:32 -07:00
Neale Pickett 12a6398193 CI/CD 2023-10-27 21:22:10 -07:00
Neale Pickett 78dbf8228d CI/CD 2023-10-27 21:20:18 -07:00
Neale Pickett eac1556240 CI/CD 2023-10-27 21:19:23 -07:00
Neale Pickett eca419bd13 CI/CD 2023-10-27 21:16:22 -07:00
Neale Pickett 9aad1dca4b CI/CD 2023-10-27 21:15:42 -07:00
Neale Pickett 99d4c62e26 CI/CD 2023-10-27 21:13:14 -07:00
Neale Pickett 32ae4d2bc8 CI/CD 2023-10-27 21:10:28 -07:00
Neale Pickett a0a5e11c50 CI/CD 2023-10-27 21:08:21 -07:00
Neale Pickett c7fa910d21 CI/CD 2023-10-27 21:05:48 -07:00
Neale Pickett 59055735e5 CI/CD 2023-10-27 20:36:48 -07:00
Neale Pickett 64778580b3 CI/CD 2023-10-27 20:33:32 -07:00
Neale Pickett bf5f3f5d0d CI/CD 2023-10-27 20:25:47 -07:00
Neale Pickett 9cd9ee1c94 CI/CD 2023-10-27 20:19:56 -07:00
Neale Pickett 76d32348ee CI/CD 2023-10-27 20:02:54 -07:00
Neale Pickett 0a8ac0aa02 CI/CD 2023-10-27 20:02:16 -07:00
Neale Pickett 8874ea1473 CI/CD 2023-10-27 20:01:46 -07:00
Neale Pickett 394ecdeb6c CI/CD 2023-10-27 20:00:59 -07:00
neale 0eb51f795e Update .gitea/workflows/build.yaml 2023-10-27 20:56:18 -06:00
neale e0035bd374 Update .gitea/workflows/build.yaml 2023-10-27 20:53:16 -06:00
neale 90c1f3ab77 Update .gitea/workflows/build.yaml 2023-10-27 20:52:19 -06:00
neale faeef87787 Update .gitea/workflows/build.yaml 2023-10-27 20:33:11 -06:00
neale da59a3ab40 Update .gitea/workflows/build.yaml 2023-10-27 15:24:34 -06:00
neale c3b6c853ae Update .gitea/workflows/build.yaml 2023-10-27 15:04:19 -06:00
neale 1d20f20ed2 Update .gitea/workflows/build.yaml 2023-10-27 14:58:27 -06:00
neale 648b71d43e Update .gitea/workflows/build.yaml 2023-10-27 14:58:09 -06:00
neale 74e09fb8ca Update .gitea/workflows/build.yaml 2023-10-27 14:56:06 -06:00
Neale Pickett 7881dd6506 Add gitea workflow 2023-10-27 13:53:05 -07:00
Neale Pickett dfadfd3790 Merge branch 'main' of https://git.woozle.org/neale/homepage 2023-10-22 11:19:17 -06:00
Neale Pickett e8377d7441 Add Mt Shasta photo 2023-10-22 11:19:16 -06:00
Neale Pickett 30dd0393a1 Merge branch 'main' of https://git.woozle.org/neale/homepage 2023-10-22 11:18:51 -06:00
Neale Pickett dd4fe6c7a1 How train gets to Pasco 2023-10-21 17:10:57 -06:00
Neale Pickett 18f9251934 Cut down reunion post 2023-09-26 12:21:25 -06:00
Neale Pickett c92130b183 High school reunion 2023-09-26 10:31:11 -06:00
Neale Pickett 5ca3731ca4 new job 2023-09-25 18:03:24 -06:00
Neale Pickett dbf7546159 typo 2023-09-25 17:26:03 -06:00
Neale Pickett 95af63afb0 Reorganize blog by year 2023-09-25 17:23:49 -06:00
Neale Pickett 1ceaba75c6 Better tags 2023-09-25 17:14:16 -06:00
Neale Pickett 7fc2aad59c Tags do something, amtrak edits 2023-09-25 16:07:41 -06:00
Neale Pickett fd0b6021b9 Moved baud into git 2023-09-25 12:42:53 -06:00
Neale Pickett e1dabd53d3 Expand baud about page 2023-09-25 12:33:48 -06:00
Neale Pickett c05c2a85b6 business travel on amtrak 2023-09-25 12:23:31 -06:00
Neale Pickett 7ba01e959f I rewrote my modem/baud simulator 2023-09-12 16:35:23 -06:00
Neale Pickett c905360ecc fire tweaks 2023-06-23 16:50:00 -06:00
Neale Pickett ef220d3ba7 Smaller file for fire (crappier too but meh) 2023-06-23 16:36:35 -06:00
Neale Pickett cc109a1996 Add WIP blog post on time_t 2023-06-23 16:22:35 -06:00
Neale Pickett d70f273f59 Fix fonts. Maybe. 2023-06-23 16:22:19 -06:00
Neale Pickett 60f8fdf06d Merge branch 'main' of https://git.woozle.org/neale/homepage 2023-06-23 16:08:09 -06:00
Neale Pickett 71dcb39d70 Fix emoji on Windows 2023-06-23 16:06:56 -06:00
Neale Pickett fe2bc2937b Kaktovik post 2023-04-14 08:25:01 -06:00
Neale Pickett 838ffd9987 Merge branch 'main' of https://git.woozle.org/neale/homepage 2023-04-13 15:36:25 -06:00
Neale Pickett 68a8292a6c date format + trains 2023-03-14 09:33:00 -06:00
Neale Pickett 123b16bf50 Add triscit 2023-03-01 18:46:18 -07:00
Neale Pickett 5b497bbea5 love boat 2023-01-30 17:54:04 -07:00
Neale Pickett da91e784f9 love boat 2023-01-30 13:21:33 -07:00
Neale Pickett dac295d522 more love boat 2023-01-26 08:24:57 -07:00
Neale Pickett b9be541bdb Another episode of love boat 2023-01-24 17:54:14 -07:00
Neale Pickett 64252ba6ac tone it down 2023-01-24 16:27:58 -07:00
Neale Pickett ee54d12e2d emphasis 2023-01-24 16:17:53 -07:00
Neale Pickett 4e4feacf45 emphasis 2023-01-24 16:16:52 -07:00
Neale Pickett 9d297ea280 love boat, photography, fix images 2023-01-24 16:13:28 -07:00
Neale Pickett 4f8873b3dd I tried to watch Dr Who again 2023-01-12 14:41:25 -07:00
Neale Pickett 29fe50152e Merge branch 'main' of https://git.woozle.org/neale/homepage 2023-01-12 13:58:32 -07:00
Neale Pickett b4c1dcb026 Yurt story 2023-01-07 16:12:28 -07:00
Neale Pickett 483e6006ef Smartwatches 2023-01-04 16:02:12 -07:00
Neale Pickett b11522b2da add cheating opinion 2022-11-06 11:47:23 -07:00
Neale Pickett eecda2345e fix phrasing, soften tone a midge 2022-10-28 15:48:08 -06:00
Neale Pickett 764e04f3e1 touch-up on CLRG post 2022-10-28 15:41:47 -06:00
Neale Pickett 9282dc3cdd curmudgeon blog post 2022-10-28 15:02:06 -06:00
Neale Pickett 934b974d10 Add feisresults, almost complete 2022-10-10 19:59:25 -06:00
Neale Pickett 6d0ebb6194 Got guidebook parsed, now to calculate placement 2022-10-10 19:20:09 -06:00
Neale Pickett 9f4b2c989c Polish up FeisWorx parser 2022-10-10 15:53:55 -06:00
Neale Pickett 87f7c5ffb8 Working parser for one dataset 2022-10-10 13:19:19 -06:00
Neale Pickett b8958b958f Split yesterday's post into today's 2022-10-10 07:21:53 -06:00
Neale Pickett 097881f1bd now? 2022-10-09 22:54:56 -06:00
Neale Pickett 6b9e27d44f can I sleep yet 2022-10-09 22:49:58 -06:00
Neale Pickett 3603c7e9cb Adjust ranking offsets 2022-10-09 22:34:44 -06:00
Neale Pickett 80d24b1011 Comment on weird section 2022-10-09 21:47:37 -06:00
Neale Pickett 39f3fe3de7 Remove dumb disclaimer 2022-10-09 21:44:24 -06:00
Neale Pickett 9e972a1e2f Okay, that's enough for today. 2022-10-09 21:42:30 -06:00
Neale Pickett f1087f96e4 CLRG Scoring Analysis pre-draft 2022-10-09 20:59:33 -06:00
Neale Pickett 76c254c4e3 Names to variables 2022-10-05 12:28:28 -06:00
Neale Pickett 59a46d2f5b I say probably too much 2022-10-05 12:21:02 -06:00
Neale Pickett 432ec4a40e cleanup 2022-10-05 00:57:32 -06:00
Neale Pickett 71166100bc cleanup 2022-10-05 00:56:03 -06:00
Neale Pickett d31ff56b03 Stick to facts 2022-10-05 00:52:24 -06:00
Neale Pickett 31ecaa667a CLRG cheating essay 2022-10-05 00:48:12 -06:00
Neale Pickett 0930c517fd Cars update 2022-10-02 08:02:52 -07:00
Neale Pickett df8d5f842a add shortcodes 2022-10-02 08:54:49 -06:00
Neale Pickett e0a6e1596b Merge branch 'master' 2022-10-02 07:47:27 -07:00
Neale Pickett 0118276f63 Add cars blog post 2022-10-02 07:39:43 -07:00
Neale Pickett 3ec547b720 Susan video clip in new shortcode 2022-09-06 12:32:48 -06:00
Neale Pickett 38e9bcdb83 Truck bling 2022-09-06 12:29:18 -06:00
Neale Pickett c8d67929fc Mobile-friendly nav 2022-09-05 08:34:52 -06:00
Neale Pickett 05da637e16 Add README 2022-09-05 08:07:31 -06:00
Neale Pickett c71a8ae6ba This thing serves no purpose but making me angry every time I stumble across it 2022-09-05 07:53:43 -06:00
Neale Pickett aedcb9f83e also uninvert canvas 2022-09-04 20:39:56 -06:00
Neale Pickett 161d81ac48 blog: theatrical lights 2022-09-04 20:22:35 -06:00
Neale Pickett 8122ae2fdf uninvert video too 2022-09-04 20:06:14 -06:00
Neale Pickett c53e56d2e7 Stop escaping header HTML 2022-09-04 20:04:25 -06:00
Neale Pickett 0183537207 Dark mode 2022-09-04 19:54:41 -06:00
Neale Pickett 9217613600 Remove cruft 2022-09-04 17:06:22 -06:00
Neale Pickett 26b463846b Add run script 2022-09-04 17:03:46 -06:00
Neale Pickett 67caf07d9c Move to Hugo, then move to self-hosted 2022-09-04 16:59:13 -06:00
Neale Pickett 7b0a6c4ddd poopy sheet music 2022-08-09 21:14:09 -06:00
Neale Pickett 83dcd49fc2 that was a dumb blog entry 2022-08-09 20:31:44 -06:00
Neale Pickett ea9bf201ac use margin for spacing, not padding 2022-08-09 20:14:33 -06:00
Neale Pickett 7b95934b14 Flexbox nav menu 2022-08-09 20:12:34 -06:00
Neale Pickett 7b382d470c responsive video/image sizing 2022-08-09 19:03:01 -06:00
Neale Pickett af47868004 finally, got autoplay working 2022-08-09 07:39:02 -06:00
Neale Pickett a1e047e727 okay, don't indent html I guess 2022-08-09 07:26:50 -06:00
Neale Pickett d07b6d9ed9 Fix Susan video 2022-08-09 07:23:37 -06:00
Neale Pickett 3186b91cbf thoughts about networked computers 2022-08-05 11:55:56 -06:00
Neale Pickett 7315fd5319 Mention that I ride slowly 2022-08-03 13:07:46 -06:00
Neale Pickett 8c78f8ae9b Talk about my bicycle 2022-08-03 13:06:21 -06:00
Neale Pickett fa4c2a2af6 Oops, add blog layout 2022-08-02 15:31:02 -06:00
Neale Pickett a3f2ccc89c List out tags 2022-08-02 15:23:06 -06:00
Neale Pickett 806e27345e Move blog posts into a blog directory 2022-08-02 15:02:45 -06:00
Neale Pickett 6889078c15 Add blag link 2022-08-02 14:31:42 -06:00
Neale Pickett b5f6d90847 Add an RSS feed 2022-08-02 14:09:35 -06:00
Neale Pickett 188aca58ce Update run.sh 2022-08-01 16:33:37 -06:00
Neale Pickett 8b5147da34 I'm bringing blogging back. (Yeah!) 2022-08-01 16:18:29 -06:00
Neale Pickett cdd900f568 Update grepdict.js 2022-01-25 07:15:48 -07:00
290 changed files with 6783 additions and 1023 deletions

View File

@ -0,0 +1,25 @@
name: Homepage
on: [push]
jobs:
publish:
runs-on:
- hugo
timeout-minutes: 1
steps:
- name: Set up SSH
run: |
mkdir -p $HOME/.ssh
cat > $HOME/.ssh/known_hosts <<EOD
${{ secrets.SSH_KNOWN_HOSTS }}
EOD
umask 077; cat > $HOME/.ssh/id_rsa <<EOD
${{ secrets.SSH_PUBLISH_PRIVATE_KEY }}
EOD
md5sum $HOME/.ssh/*
- run: git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
- run: hugo
- name: Install built site
run: |
eval $(ssh-agent)
ssh-add
rsync --delete -vax ./public/ neale@melville.woozle.org:/srv/www/woozle.org/

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
.hugo_build.lock
*~
_site/
public/

1
CNAME
View File

@ -1 +0,0 @@
woozle.org

View File

@ -1,14 +1,6 @@
---
title: README
---
This is my "homepage", which lives at https://woozle.org/
My Homepage
===========
I've had a homepage since around 1996,
but only started using git in 2008.
Earlier revisions are therefore lost to the mists of time.
This is the source to everything on
[my homepage](https://woozle.org/neale/).
It basically just slaps a header and footer on
markdown files in various directories.
I like JavaScript.

View File

@ -1,6 +0,0 @@
defaults:
-
scope:
path: ""
values:
layout: "default"

View File

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic">
<!-- My stuff -->
<link rel="stylesheet" media="screen" href="{{ '/assets/css/default.css' | relative_url }}">
<link rel="icon" type="image/png" href="{{ '/assets/images/face.png' | relative_url }}">
{% if page.scripts %}{% for js_file in page.scripts %}
<script src="{{ js_file }}"></script>
{% endfor %}{% endif %}
{% if page.headers %}{% for header in page.headers %}
{{ header }}
{% endfor %}{% endif %}
<title>{{ page.title }}</title>
</head>
<body>
<h1 id="title">
<span>{{ page.title }}</span>
</h1>
<main id="content">
{{ content }}
{% if page.Time-stamp %}
<p id="timestamp">Last modified: {{ Time-stamp }}</p>
{% endif %}
</main>
<footer>
<nav class="left">
<ul>
<li><a href="{{ '/tartans/' | relative_url }}" title="AKA Plaids">Tartans</a></li>
<li><a href="{{ '/poems/' | relative_url }}" title="I won't quit my day job">Poems</a></li>
<li><a href="{{ '/papers/' | relative_url }}" title="Various writings">Papers</a></li>
<li><a href="{{ '/toys/' | relative_url }}" title="Dumb apps">Toys</a></li>
</ul>
</nav>
<nav class="right">
<ul>
<li><a href="https://github.com/nealey/">GitHub</a></li>
<li><a href="mailto:neale@woozle.org">Email</a></li>
</ul>
</nav>
</footer>
</body>
</html>

View File

@ -1,138 +0,0 @@
/* http://www.colourlovers.com/palette/92095/Giant_Goldfish */
html {
background-color: white;
background-image: url("../images/bg.jpg");
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
background-position: center;
}
body {
background-color: rgba(255, 255, 255, 0.8);
margin: auto;
padding: 1em;
border-radius: 1em;
max-width: 35em;
font-family: "Lato", "Roboto", sans-serif;
font-size: 13pt;
}
input {
font-family: "Lato", "Roboto", sans-serif;
font-size: 13pt;
border: 0;
outline: 0;
background: transparent;
border-bottom: 1px solid black;
}
#title, td.main {
background-color: #e0e4cc;
border-radius: 0.25em;
padding: 0.1em 0.5em;
margin-top: 0;
box-shadow: 0.2em 0.2em 1em rgba(0,0,0,0.1);
}
h1, h2, h3, h4, h5, h6 {
color: #e64;
}
a {
color: #38d;
text-decoration: none;
}
a:visited {
color: #579;
}
a:hover {
color: #f83;
}
img {
max-width: 60%;
}
img.face {
margin: 1em;
width: 10em;
}
pre {
background: #e0e4cc;
overflow-x: auto;
}
blockquote {
background: #e0e4cc;
padding: 0.1em 1em;
border-radius: 0.4em;
}
footer {
clear: both;
border-top: solid black 1px;
}
nav ul {
padding: 0;
margin-top: 0;
margin-bottom: 1em;
}
nav li {
display: inline;
padding-right: 1em;
}
.left {
float: left;
}
.right {
float: right;
}
/* Formatting */
p, li, dd {
text-align: left;
}
body > p {
text-indent: 1.5em;
}
h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p, ol + p, ul + p, pre + p, blockquote + p, img + p {
text-indent: 0;
}
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
}
pre {
margin: 1em 2em;
padding: 1em;
border: solid black 1px;
}
.figure {
float: right;
padding: 0.25em;
margin: 0.5em;
font-size: small;
border: solid black 1px;
}
#timestamp {
font-size: small;
text-indent: inherit;
}
button.big {
padding: 0;
border: 0;
margin: 1em;
font-size: 2em;
background: inherit;
}

17
config.yaml Normal file
View File

@ -0,0 +1,17 @@
author:
name: Neale Pickett
email: neale@woozle.org
baseURL: https://woozle.org/
disablePathToLower: true
languageCode: en-us
title: Neale Pickett
enableGitInfo: true
markup:
goldmark:
renderer:
unsafe: true
# This breaks inline images
#uglyurls: true

View File

@ -11,7 +11,7 @@ Software
* [Neale's Projects](https://github.com/nealey/)
* [Dirtbags Projects](https://github.com/dirtbags/)
* [9wm Projects](https://github.com/9wm/)
* [Reply-To Munging Still Considered Harmful. Really.](papers/reply-to-still-harmful.html)
* [Reply-To Munging Still Considered Harmful. Really.](/papers/reply-to-still-harmful.html)
Something Else
------------

View File

@ -2,11 +2,7 @@
title: Neale Pickett
---
<!--
PGP: 1024R/EA86E211 47 67 11 06 2E 6C D8 56 2A BD D7 FC C1 D2 84 BA
-->
![](assets/images/face.png){: .face .left}
![](assets/images/face.png)
I write computer programs.
When bad guys break into a computer,
@ -15,3 +11,7 @@ I help figure out what they did.
I also teach people about programming and math.
You can find more about the teaching at
<https://dirtbags.net/>.
---
Ginnie now has a [salad blog](/ginnie/)!

View File

@ -0,0 +1,277 @@
---
date: "2004-10-27T00:00:00Z"
title: Debian on Digimatrix
---
Introduction
------------
We have a Digimatrix hooked up to our high-definition television. We
use it for a number of things:
* Watching DVDs
* Playing music from OGG and MP3 files
* Recording and playing back television shows
* Storing pictures and movies from our digital camera
* Hosting our pictures and movies, along with those of our friends,
on a web server
* Playing Super Nintendo video games
* Occasional desktop tasks
MythTV and Freevo vs. KDE
-------------------------
This sounds like a MythTV box, right? I tried MythTV, and Freevo, and
while I was impressed with how pretty they were, I just couldn't get
them working quite right.
[MythTV](http://www.mythtv.org/) gave a blank screen trying to watch or
record TV, with no useful error messages, and try as I might I just
couldn't figure out what the problem was. After spending three days on
it I gave up.
[Freevo](http://freevo.sourceforge.net/) had its own problems; I don't
remember exactly what they were now. I do remember feeling like Freevo
was held together with bailing wire and chewing gum, while MythTV had a
much more polished and easy-to-use feel to it.
Neither of them worked well out of the box with the Digimatrix remote
control.
In the end, I decided it'd be easier for everybody if we just used KDE.
It lets me bind actions to the key sequences the remote control sends
out, and for most TV-type activities the remote does enough to move
around in [Xine](http://xinehq.de/) and
[Konqueror](http://www.konqueror.org/). The
[x2x](http://x2x.dottedmag.net/) program allows us to use our laptops to
control things over the wireless network, when the remote doesn't
suffice. The Unix `at` utility and a little shell script lets us
schedule TV recordings with more ease than a VCR.
Best of all, there's no meta-information filed away in some obscure
database layout. Everything is a file somewhere and can be manipulated
with Konqueror or the shell.
This turns out to be a very workable setup.
### Installing the base system
I had to install the testing (etch) installation CD for it to have a
driver for the SiS 900 10/100 network card. Installation went smoothly
enough. Everything is detected at boot time except:
* LED Panel and front panel buttons (including volume)
* Infrared receiver
* 802.11b card ([Ralink rt2400 chipset](http://rt2x00.serialmonkey.com/)).
While I've gotten the rt2400 drivers to work, the card won't seem to
associate with my WAP until some other device does it first. This is
so inconvenient that I've run a long CAT-5 cable under the house so I
can use the 10/100 ethernet port.
I installed the following additional packages to help me administer the
machine:
<pre>
# apt-get install less zile screen ssh strace sudo ntp ntpdate
</pre>
Web server
--------------
Waldorf needed to run a web server to host photo albums.
<pre>
# apt-get install mathopd stunnel4 php4-cgi rssh
# apt-get install netpbm jhead exiftran libjpeg-mmx-progs libjpeg-progs
</pre>
I won't detail my web server configuration here, since that's unique to me.
X
----
<pre>
# apt-get install x-window-system
</pre>
To my surprise this brought in x.org. I run an HDTV over DVI, so to get
full screen, I had to change the configuration as follows:
<pre>
Section "Device"
Identifier "Generic Video Card"
Driver "sis"
Option "ForceCRT1Type" "DVI-D"
EndSection
Section "Monitor"
Identifier "Generic Monitor"
Option "DPMS"
HorizSync 30-65
VertRefresh 30-60
ModeLine "720p" 74.160 1280 1352 1392 1648 697 725 730 750
EndSection
</pre>
I also had to add "720p" to the `Modes` of the `Display` subsection of
`Screen`.
You may notice I only have 697 pixels vertically. That's because my TV
puts about 23 lines in the "overscan", preventing me from seeing my KDE
toolbar. I haven't yet found a way to recenter the screen, this just
chops off the bottom. As a hack, I put empty KDE toolbars on the top,
left, and right borders. This keeps windows inside the viewable area.
### KDE
I like KDE. I've tried MythTV and Freevo and found it's just easier to
run KDE and occasionally use a mouse and keyboard. The only thing we
don't get is a snazzy interface to recording TV shows, but we don't tend
to want to do that very often. I may work on a web interface to XML-TV
listings later on.
<pre>
# apt-get install kde kdm
</pre>
I don't know if this is typical or not, but Debian's KDE went on without
asking me a single question. Kudos to the packagers.
### Sound
<pre>
# apt-get install alsa-base alsa-utils
</pre>
We use the Digimatrix's S/PDIF output (isn't it lame that it comes out
the front?). I know from a previous installation that if you just use
the defaults, you need to reboot between playing 2-channel audio and
using Xine's pass-through option to play a 7.1-channel DVD. I'm sure it
has something to do with the sound card resetting something or other.
The solution seems to be having ALSA multiplex audio, and while I don't
get why this works, work it does.
I put the following into `/etc/asound.conf`:
<pre>
pcm.asus-hw {
type hw
card 0
}
pcm.!default {
type plug
slave.pcm "asus"
}
pcm.asus {
type dmix
ipc_key 1234
slave {
pcm "hw:0,0"
period_time 0
period_size 1024
buffer_size 4096
rate 48000
}
}
ctl.asus-hw {
type hw
card 0
}
</pre>
### A decent desktop
At this point I was able to browse the web and play music using KDE's
"JuK":http://developer.kde.org/~wheeler/juk.html, so I took a break to
dance around the living room with my 1-year-old daughter as ABBA sang to
us.
### Playing DVDs
I like kaffeine, mostly because it's part of KDE and I'm a purist. It
can play all sorts of movies and even has a nice startup screen that
allows you to type in numbers for various actions (play from playlist,
play DVD, etc.).
<pre>
# apt-get install kaffeine
</pre>
### DVD drive speed ###
Linux does not use DMA on IDE devices when it boots up, you have to turn
that on yourself. I'm not sure what the reason is for this, probably
compatibility with some ancient thing that blows up if you attempt DMA.
In any case, turning on DMA will allows your DVD drive to keep up with
the data on the DVD.
<pre>
# apt-get install hdparm
</pre>
To turn on DMA, I put the following at the end of `/etc/hdparm.conf`.
While I was at it, I turned ot DMA for the hard drive too.
<pre>
/dev/dvd {
dma = on
}
/dev/hda {
dma = on
}
</pre>
### Try a DVD ###
At this point I was able to watch DVDs, so I did. I watched the first
DVD of the first season of Buffy, which turned out to be a terrible idea
since it's pretty dark and not very high quality. I played around with
the gamma settings in the KDE configuration tool, and set my gamma at
1.25. Then I adjusted the brightness, contrast, and saturation of
Kaffeine, and got what I think is a pretty nice-looking configuration.
### The remote control
What good is a home theater system if you have to get up off your butt
to press buttons? This is America, man! I want to be able to eat
cheez-doodles and watch porn all night without having my feet hit the
floor, ever.
### The keyboard thingy ###
My Digimatrix came with this IR receiver thingy that goes in between the
keyboard and the keyboard port on the Digimatrix. It synthesizes
keypresses in response to your remote. Pretty slick! This is good
enough for most things, and for a long time I just bound remote
keystrokes to do certain things in KDE applications, and to certain
actions in Xine.
### lirc ###
lirc is the Linux Infra-Red Control system. It provides a standard
interface to various IR recievers and remote controls, and supplies
events to whatever wants to listen to it. KDE has a module to listen to
it, so I figured I'd give it a go.
# apt-get install kdelirc
Unfortunately, lirc 0.7 (the version in debian testing and unstable)
does not compile on Linux 2.6.12 and newer, so I had to install from
source code.

View File

@ -1,5 +1,9 @@
---
title: Every Episode of Doctor Who
date: "2021-12-22T00:00:00Z"
tags:
- dr-who
- tv
title: All of Doctor Who
---
I'm going to try to watch every episode of Doctor Who, in order.
@ -13,8 +17,3 @@ A few rules I'm setting out initially:
and I'm not going to feel bound to try and attain said.
* I will be viewing this though my personal biases.
* I may create more rules later on.
---
* [Season 1](season01.md) - December 2021
* [Season 2](season02.md) - December 2021

View File

@ -0,0 +1,39 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E01-E04: Cavemen and Fire'
---
I guess this first story pretty clearly established that they travel
through time.
## S01E01 - An Unearthly Child
Oh man, the pacing is just slow.
I like how they're exploring "what if a time-travelling alien disguised itself as a human and went to high school."
Okay, there are two high school teachers,
and this highschool aged timelord girl who's the Doctor's grand-daughter.
The doctor is kind of a jerk.
## S01E02 - The Cave of Skulls
Right, okay, cavemen. And of course they're all short-term-memory idiots.
Okay, sure.
## S01E03 - The Forest of Fear
I forgot what this one was about,
because I didn't decide to write things down until later.
## S01E04 - The Firemaker
The man teacher makes a fire because of course the man does that in 1963.
And then the leader of the cavemen goes hunting.
They make some skulls on fire to distract everybody,
which is pretty cool,
and then they escape.

View File

@ -0,0 +1,82 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E05-E11: Daleks!'
---
Holy crap! I didn't realized the Daleks went all the way back to episode 4!
Also there are some Dutch-looking people who are of course the good guys.
## S01E05: The Dead Planet
They find a dead city which, oops, high background radiation.
Cool doors open up and some neat architecture.
You know what, for the time and (I assume) paltry budget of the first season,
this is pretty badass.
This Doctor is a jerk. Faking a broken part to force everyone to do what
he wants.
## S01E06: The Survivors
Daleks! It's cool that they waited until the second episode to bring them out.
They're pretty clearly assholes right at the outset here.
No moral ambiguity with the Daleks.
The Doctor is pretty frail. Honestly, the rest of the crew seems more
important than he is.
## S01E07: The Escape
I think we really meet the Norwegians here. I don't recall exactly.
I'm pretty sure this is the one where
the Norwegians try to meet up with the Daleks and forge a peace treaty,
not knowing the Daleks are complete assholes.
## S01E08: The Ambush
They start taking out the Daleks. There appears to be some sort of
creature inside, which can be taken out, and the dude teacher crawls in.
This is actually pretty cool, I liked this one.
## S01E09: The Expedition
I think the guy teacher tries to convince the peaceniks to fight the Daleks,
by presenting them with the possibility of him screwing up their stuff.
The Norwegians traipse around through some cool looking sets to
look like an alien landscape.
I think this is when the Daleks realize they need high background radiation
in order to survive, because the Norwegians (the Thaals I guess) anti-radiation
drug kills them. They plan to dump ionizing radiation onto the planet's
surface.
A Thaal dude goes to get water and gets sucked up by a vortex or something.
## S01E10: The Ordeal
Half the episode was watching people jump over a chasm in a cave.
The pacing on this, holy crap.
I think this is the one where the Doctor screws up their electrical grid.
## S01E11: The Rescue
Everybody runs around in the cave some more, and eventually they pop out
in the Dalek city.
The Doctor and his Grand-daughter
are imprisoned and he volunteers to teach them about his ship and how to
travel through time if they let him go.
The Norwegians and teachers rain holy hell and kill all the Daleks.
Everybody goes home.
Honestly, man, for being written 60 years ago, I'm surprised by how
compelling the Daleks were written when viewed by me today.

View File

@ -0,0 +1,31 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E12-E13: A Tardis Button Gets Stuck'
---
This story was bathouse crazy.
Part of it was probably that I wasn't paying full attention,
but it was also just nuts.
## S01E12: The Edge of Destruction
People alternate between normal and crazy.
There's a lot of screaming.
Random weirdness takes place and it doesn't make sense.
Everybody turns of everybody and then suddenly it's fine and dandy.
## S01E13: The Brink of Disaster
The Doctor starts piecing together some clues.
Turns out the Tardis itself has been trying to send them a message
by dropping obscure clues.
He figures it out and unsticks the "go back to where you just were" button,
which, because it was stuck down, sent them back to the origin of the
solar system.
You know, not awful.
It felt pretty incoherent but I was willing to go along.

View File

@ -0,0 +1,41 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E14-20: Doctor Who goes to China'
---
I guess the film for this was lost,
so it was sort of a slideshow playing over a radio drama.
## S01E14: The Roof of the World
I made it as far as them getting into a hut in Mongolia and meeting
Marco Polo,
and then I skipped to the next story.
## S01E15: The Singing Sands
More British people pretending they're in China, I presume.
## S01E16: Five Hundred Eyes
Skipped
## S01E17: The Wall of Lies
Skipped
## S01E18: Rider from Shang Tu
Skipped
## S01E19: Mightly Kublai Khan
Skipped
## S01E20: Assassin at Peking
Skipped

View File

@ -0,0 +1,99 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E21-26: Finding the Chips'
---
The Scooby gang has to find some microchips scattered around the planet,
so a machine can be repaired to mind-control everyone into
being peaceful.
## S01E21: The Sea of Death
This was the episode during which I decided to start writing this.
It's really interesting how they kept the sort of stumbling around delivery
of lines, rather than shooting another take. I kind of like it.
It's like watching a play.
LOL at the alien costume with swim flippers.
Frickin' Susan is, disappointingly, pretty much a horror movie character.
Just sort of blithely wandering around all over the place and screaming
a lot. Amusingly, they just assumed it was her from her scream.
Okay, got the main plot point. The dude needs to recover some keys so they
can mind-control people to make everybody peaceful. I really hope they don't
just accept this without question.
Their mime acting could use some continuity work, LOL.
## S02E22: The Velvet Web
When the statue's eyes lit up and the creepy lady came out to put rocks
on everybody's forehead, it really felt like a 1970s episode from when
I was a kid.
Aww yeah, brains in jars getting killed.
## S02E23: The Screaming Jungle
The Screaming Jungle, eh? Must be about Susan.
For crying out loud, yep, she's screaming again.
I read that this actress quit early on.
I don't blame her. What a limiting role to have to play.
Now Barbara's screaming. A statue grabbed her.
Now she's screaming again. A net fell on her.
Third time screaming. Some plants grabbed her.
Apparently the scream in the jungle was mostly Barbara.
## S01E24: The Snows of Terror
Okay, now Ian and Barbara are cold. And a beardo has rescued them.
Ian left for some reason,
and Beardo's giving off some really rapey vibes.
Oh, sure enough, he's got all their junk.
Susan and a blonde woman are in a cave.
I've decided I don't like Susan at all,
her sole role appears to be screwing things up and screaming.
Oh boy. They're all in another cave, and they're going to explore it..
Oh, okay, we don't have to watch that. They melt some ice and...
Susan's screaming again as some dude with a cardboard sword wakes up.
Great, they found another key.
## S01E25: Sentence of Death
The Scooby gang vs. the Japanese Criminal System, I guess.
They assumed Ian is guilty and he has to prove he's not.
The Doctor is going to be his defense.
I've seen this episode at least three times on Star Trek,
and once on the Brady Bunch.
I might skip this one.
Oh, neat, a dude who hits his wife.
Okay, but you know what, I dig the costumes.
## S01E26: The Keys of Marinus
I don't like Barbara's hairstyle, though.
Oh, snap! What a twist! I seriously was not expecting this one.
I mean, I probably should have, from a casting standpoint, but I'm not
really devoting a lot of mental energy to this.
Okay, I guess they decided the mind-control machine wasn't so great.
Kind of a naive take but whatever, it was a pretty fun story.

View File

@ -0,0 +1,68 @@
---
title: "Doctor Who S01E27-30: Doctor Who goes to Mexico"
date: 2021-12-23
tags:
- dr-who
- tv
---
A bunch of white people pretend to be Aztecs, and explore their moral
perspectives on human sacrifice.
To its credit, there is at least lip service to the notion that
this is some other culture and maybe you can't impose your current moral
structure over it.
## S01E27: The Temple of Evil
Oh goodie. Barbara's gone and gotten herself in trouble again.
I'm not sure I can stomach this one.
Yay, let's focus on human sacrifice. And let's frame it as summoning rain,
because I'm sure that was the beginning and end of the philosophy behind it.
LOL the sword fight.
Haha, Susan's screaming desecrated an Aztec temple.
## S01E28: The Warriors of Death
Damn, the Doctor's pissed.
Holy crap, the Vulcan Nerve Pinch! 2 years before it showed up in Star Trek!
More 1960s dudes wrasslin'. But there's a twist, he got scratched.
## S01E29: The Bride of Sacrifice
Aha, the old Saffron maneuver, eh? Looks like the Doctor's gettin' hitched!
The gang has a moral dilemma about altering the culture of the Aztecs by
Barbara abusing her god status to outlaw human sacrifice. The Doctor gets
married or engaged or something. Susan finally expresses an emotion other
than terror:
{{< video src="susan-well-hello-there.mp4" text="Well, hello there." >}}
I'm back to being uncomfortable with the cultural framing here. I mean,
maybe this is useful for viewing 1960s British culture, but that's still
too recent for me to not feel squicky about it. The only reason I didn't
skip this entire story is because Wikipedia said it was one of the best
stories of the entire franchise.
## S01E30: The Day of Darkness
They found a tunnel or something, to get back into the burial chamber
containing the Tardis. Susan is going to get married or sacrificed or
something. Ian does the Vulcan Nerve Pinch on a white guy dressed as a
brown guy, saving the white lady. They find a way to escape, and
somebody gets sacrificed, and then Barbara gets to watch Susan get
sacrificed or something, I'm not sure.
Oh, more 1960s dudes fighting. I skipped it. I presume the British
high school teacher beats the Aztec warrior who's been training his
entire life.
Barbara has some sort of moral quandry about her inability to change
the past, which I guess was the point of the story. I sure hope
they don't wind up on earth again in the next story.

View File

@ -0,0 +1,48 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E31-36: The Sensorites'
---
### S01E31: Strangers in Space
They're in a spaceship, thank goodness. The lighting is pretty bad.
Okay there are these sensorites messing with peoples' minds. Aw, crap,
we're sort of on Earth. At least it's in the future.
This is a cool premise,
taking over a ship by manipulating everybody's emotions.
### S01E32: The Unwilling Warriors
Hey! Creepy! I like this story.
Susan is doing something! Nice!
Susan makes a sacrifice
### S01E33: Hidden Danger
Susan needs to get out of this place! The Doctor's being a real jerk. Again.
Seems like she's growing up and needs her own space.
Anyway. The Sensorites are cool. And I'm glad Barbara seems to get an equal
say in matters. Like, she just argued with Ian about something, and she was
like "well then it's settled, we'll do it my way".
Still can't abide her hairstyle, though.
Ian seems really British. It's not difficult to imagine pretty much
everything he does being done by an older dude with a brandy in his hand.
He doesn't seem to get to grow as a character,
he's already fully grown, I guess.
I dig that the sensorites have internal debates.
## S01E34-36: More Sensorites
I guess I didn't write anything down here.
I really enjoyed this story, though!
A marked contrast to the rest of this season.

View File

@ -0,0 +1,16 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E37-42: Doctor Who vs the French Revolution'
---
Ugh, I just couldn't get myself to care about another one of these.
I'm noticing there are two basic plots here:
* Doctor Who goes back in time on Earth (I don't like these)
* Doctor Who goes to some other planet (These are cool)
I guess that's still true today, in 2022.

View File

@ -1,9 +1,11 @@
---
title: Doctor Who: Season 2
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S01E01-E03: Honey, I shrunk Doctor Who'
---
# Story 1: Honey, I shrunk Doctor Who
## S02E01: Planet of Giants
They're on Earth again, but they're tiny.
@ -23,6 +25,10 @@ Holy hell, Barbara faints!
I can't stop thinking about how much fun it must have been to
build this set!
Note to self: I think this was the epsiode with the insane foley.
I should make a little video of it,
there's a full 2 minutes of just insane analog noise and nothing else.
## S02E03: Crisis
This is very cute.
@ -30,7 +36,3 @@ This is very cute.
Yeah, just store that giant poison seed next to Barbara,
who's suffering from poisoning.
# Story 2: Earth. Again. Also Daleks.
They go back to earth, and find a whole mess of Daleks.

View File

@ -0,0 +1,13 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- dr-who
- tv
title: 'Doctor Who S02E04-E09: Earth Again, and Daleks again'
---
They go back to earth, and find a whole mess of Daleks.
Everybody's got some stupid helmet that puts them under Dalek control,
but they manage to start an rebellion and some people throw Daleks around.
Maybe I should take a break before doing any more of these.

View File

@ -0,0 +1,138 @@
---
title: Photography
date: 2023-01-23
---
Downstairs, in our basement,
we have boxes and boxes of old photographs that we inherited.
We're not sure what's in the boxes:
we only have this vague notion that they're valuable,
because somebody spent time and money on them.
This is a really lousy situation for us to be in.
What are we supposed to do with these?
Could they be useful someday to our grandchildren?
Is this just the output of somebody's hobby,
with little to no value to anyone else?
How many generations must hang on to these before it's okay to discard them?
## Hobby photography over the years
My grandparents would get one photograph taken of the whole family,
maybe every 10-20 years,
if you were lucky enough to have a traveling photographer drop by your house.
So you cherished that photograph,
and you thought about your great-grandchildren seeing this marvel of modern technology.
Most of the photos we have from this era
have hand-written notes recording the year,
the location,
the people involved,
and maybe some additional context like
"this house was built by Peter and his father in 1885".
By the time my parents had disposable income,
you could get a camera for a reasonable amount of money.
The film cost money,
and getting prints developed cost money,
so they made sure everybody was lined up,
facing the camera,
and smiling,
before pressing the shutter button.
But because you could pop off 26 shots at a time,
and because the film would actually degrade if you didn't develop it quickly,
they took a lot more pictures.
They probably didn't have time to label all of them,
and anyway, that was just some dumb thing their parents did.
My generation saw digital cameras pop up
right around the time we were shopping for more expensive cameras.
You could see the photo you took immediately,
so people would take another shot when they saw that somebody had their eyes closed.
We started taking more candid snapshots,
and we didn't sweat it when we accidentally photographed a shoe or something.
We had to stop thinking of one photo as being a sort of investment:
you could take 20 photos and then just pick the best one,
and there was no difference in cost.
Then, we stopped picking the best one,
because we were busy.
## The problem with digital
These boxes of prints downstairs have outlasted every hard drive I've ever owned.
And as someone whose job is reverse-engineering undocumented file formats,
I am not holding my breath that in 500 years anyone will know what to do with a JPEG,
much less a JPEG on a FAT32 file system on a SATA hard drive.
In fact I am pretty confident that the time we're living in
will come to be seen as a dark age,
about which little is known,
because people stopped writing things in places that were easy to preserve.
We currently have 44,708 digital photos and videos: about 419 Gigabytes.
With a few exceptions of things I've scanned in from prints,
the oldest photos we have are from October 2000.
That's around 2000 photos every year,
once we had a baby and we really got going photographing every damn thing.
There's a multi-year gap before that,
and there are holes after October 2000,
because I accidentally deleted the wrong directory.
Photos I took before around 1998 are downstairs in one of those boxes of prints.
*I already have a dark age from losing digital records!*
## Getting this under control
Someday,
hopefully soon,
we're going to go through all 44,708 photos,
and file them away into a few categories:
1. Photos we're going to make physical copies of,
and place in an album,
with some sort of annotation about why it's significant.
2. Photos/Videos we're going to group together,
along with some sort of annotation about why that group is significant.
This might be a photo album,
or maybe just a directory or box.
3. Photos/Videos we're going not going to do anything special to,
with a note that these are "just in case",
without any special meaning,
and our progeny can feel okay throwing them out.
4. Photos/Videos we don't want to keep at all.
We're then going to have to sort through these hundreds of prints,
trying to decide
on behalf of our ancestors
why the photo was taken.
We'll have to file each one in the same categories.
At the end of this,
we'll have actual photo books,
with written text explaining what's significant.
I hope this leaves future generations with a better situation
than the context-free boxes of prints that we inherited!
## Why I'm doing this
I love this old silver-gelatin print
of my grandfather's entire family.
They're all gathered in front of their 900 square-foot house,
with my grandfather showing off the family's new bicycle.
Also in the photo are the family dog,
and their cow.
My hope is that
people in the future can pick up one of my albums,
spend a few minutes going through it,
and get a feel for what we looked like and how we lived.
They'll be encouraged to actually do that,
because of the *limited amount* of stuff,
the *easy interface* to viewing it (namely: posess eyes),
and the *context* I will have provided.
As a result of thinking about this for so long,
I've noticed I stopped taking so many pictures.
A lot of the pictures I do take are sent immediately and not stored:
my great-grandchildren don't really need to see this burrito,
or our dog on the table.
Maybe eventually I'll get back to storing only a few dozen photos per year,
and the future albums will be very quick to put together.

View File

@ -0,0 +1,8 @@
---
date: "2022-08-01T00:00:00Z"
published: false
title: Private Blog Pages
---
I'm not sure why I would want to make a private blog entry that's also checked into Git,
but this file will serve as a reminder to myself how to do that.

View File

@ -0,0 +1,68 @@
---
date: "2022-08-01T00:00:00Z"
title: Return of the blog
---
It was fun for a while,
but it seems like letting a big company host my blog posts wasn't actually a great idea.
So I'm starting the blog back.
I'll probably drop little updates here and there about what I'm up to.
I doubt I'm going to get into the sorts of tirades I used to.
An advantage of being older now:
I rarely feel the need to convince anybody of anything.
I thought it would be fun to pull some of my old blog entries out of the archive,
but it appears I mostly used my homepage to keep lists and record information.
Maybe one day I'll find out what happened to all that stuff I wrote.
## Self-Hosting is cool again
I've also been moving my source code back onto my own site,
removing things from my phone that demand attention,
and just generally reeling stuff back in to self-hosted.
One thing I really am not looking forward to self-hosting again is email:
SMTP is just a nightmare.
You need the SMTP server,
the IMAP server,
the anti-spam stuff,
domain certificates,
rate limiting,
and just ugh.
I may have to do email if Google really fouls up,
but I really don't want to.
## Pocket Computers: turns out, kind of a pain
The phone app removal has been interesting.
After removing the news aggregator,
I wound up having to remove news updates from the web browser app
and from the search app.
It took a couple of days to get used to not getting constant news updates,
but it seems okay now.
I can still read the news on my laptop, when I want to do that.
My phone/pocket computer has turned back into a bunch of neat tools,
like a great camera, and a way to send and receive messages to people,
and a low-stress low-stakes low-attention game I've been playing.
## Books
I really like electronic books.
$SPOUSE and $CHILD got me a new ebook reader,
to replace the 10-year-old one I've been using.
There was nothing wrong with the 10-year-old one,
but my eyesight has gotten worse,
and the low contrast screen has become difficult to read.
The new device is a Kobo Clara HD,
with better contrast.
It can also check out ebooks from the local library,
over a wireless connection.
I like it a whole lot,
and I find it amusing that the old one lasted me 10 years.
$SPOUSE has it now, she might pick it up when she finishes her current book.

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,35 @@
---
date: "2022-08-03T00:00:00Z"
title: Bicycling
---
A few recent events have conspired to push me back into cycling to work and back:
- I have to come in to work every day of the week
- My wife and daughter and both at home almost every day of the week, and don't
walk me in very far
- A road closure has made 5pm traffic stop and go for about half of the route
home
- COVID messed up my lungs and gave me an excuse to be lazy
So now I'm riding the bicycle instead of walking to work.
I ride pretty slowly, about a jogging pace. I'm in no hurry.
It takes about 20 minutes each way, as
opposed to 40 minutes walking. I imagine, after sitting in traffic, finding
parking, and then walking from the parking lot to my building, driving the car
would be 20-30 minutes. In fact, the other day, my daughter drove in to her job
downtown a few minutes before I left on my bike, and I passed her on the way in.
The bike lane is never stop-and-go traffic.
I'm riding my IKEA Sladda, which was recalled years ago due to the drive belt
breaking. I kept it, because I've had chains break before and I'm pretty sure I
can cope with a belt breaking.
The bike has been maintenance free for years, other than inflating the tires.
One day that belt will break, and I'll probably have to replace the entire
drivetrain. But the rest of the bike is solid. Hopefully it helps that I use the
front brake when I want to slow down in a hurry, no slamming back on the coaster
brake for me.
![My Sladda](Sladda.jpg)

View File

@ -0,0 +1,41 @@
---
date: 2022-08-05
title: My (online) Generation
---
I've been on this IRC channel (Signal now) for maybe 20 years now, maybe more.
We've wound up having multiple events where we meet in real life,
and I consider them probably my closest friend group;
certainly the people I've stayed in contact with for the longest.
Today (August 2022) I mentioned to them that I've dialed back how often I read the news,
and how that's really helped me,
and we came to this sort of group consensus that we're pretty atypical.
Like, most Americans are only dimly aware of what was going on in Washington
whereas I have literally lost hair from stress over it.
What I would consider to be my online generation was online way before anybody else.
Most of us were using modems to dial bulletin boards back in the 1980s.
We remember a time when, in order to get Windows connected to a network,
you had to buy separate software, and it was buggy as hell.
Most of us started using Linux in the early 1990s,
back when only weirdos would consider using Unix for their desktop machine.
It wasn't clear to us that networked computers were ever going to become popular,
and seeing things like Facebook take off seem to us like we were the early pioneers.
There's a lot more to say about what shaped our world,
but others have probably written about it more clearly.
But I feel like, as the world has begun using the Internet more,
it's probably challenging for highly online people to tease out what "normal" life is anymore.
It's so easy to look at hundreds of millions of people tweeting or posting on Facebook,
and think that represents everybody's day-to-day life,
because networked computers have been our entire existence for decades.
I'm not sure it's good for humans to be in contact with millions of other people;
tt least, it's not something we appear to be able to handling gracefully.
Maybe future generations will figure this out,
and it will bring the species together in a way the world has never seen.
We might need that to cope with what's coming in the next hundred years.
But for now,
for people like me who have spent a lot of time online for decades,
it's turning out to be nice to (re)discover smaller communities.

View File

@ -0,0 +1,50 @@
---
date: "2022-09-04T00:00:00Z"
title: Lights! ...
---
The kid has been gearing up to record an audition video for a dance company,
and I've been studying up on how to set up lights, so it looks cool.
I think I did pretty well,
for about $140 worth of parts.
It's primarily clamp lights,
and some el cheapo colored acrylic from Amazon
that I cut with $SPOUSE's CNC cutting machine.
Also some aluminum foil.
![Lights!](lights.jpg)
I know now that this is just standard 3-point lighting,
that somebody smarter than me figured out.
Looks pretty good though!
That back light provides a sort of halo, to give depth on the screen.
And the colors against the black fabric (bedsheets) provide texture.
That's about all I know about lighting so far.
For sound, we played music through a mixer,
and hard panned both channels to the left side.
The dance pad has a mic inside it,
that we hard-panned to the right.
Hopefully this makes it so the postprocessing can mix feet in with stereo music.
---
I was up until 01:00 porting woozle's stuff over to a more unified login.
You still need three different passwords for various things,
but at least now it feels a little more like a single offering.
Today I moved my web site (and this blog) off of GitHub,
which provided web hosting for free.
Now it's on my cloud VM,
which is also free (because it's so dinky small),
but should be easy enough to move elsewhere if I need to.
And with that, I think most of my work is back on stuff I own.
I tell you what, though,
WebDAV is a lot nicer than I gave it credit for.
I've got my NAS on the public Internet now,
password protected,
and being able to get to my files from anywhere is pretty handy.

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -0,0 +1,34 @@
---
title: Truck bling
date: "2022-09-06T12:11:00-0600"
---
Yesterday,
one of $CHILD's friends from dance drove up so I could help her bling out her truck.
We used WS2811 LED strings with adhesive backing.
I flashed a Seeeduino Xiao with CircuitPython,
set up an example program,
and let the two of them go nuts writing light animations.
Then we went out and stuck the stuff to the truck,
reinforcing it with zip ties for when the adhesive inevitably loses its grip.
The moms braided some wire into 3-connector cables,
the friend soldered a bunch of stuff together,
and we plugged it in.
It friggin' worked!
{{<video src="truck-bling.m4v" text="Pickup truck with color-changing ground effects">}}
The really cool part, at least for me,
is that now she can hang out with her laptop in the cabin,
reprogramming her ground effects to do whatever she wants.
Like, with a Chromebook, even.
Or a mobile phone.
CircuitPython is crazy inefficient,
but it made this stuff so easy.
Phase 2 is going to be getting a Bluetooth Adafruit chip,
setting it up to pair to their phones,
and then letting them control the color and pattern with a cell phone app.
Then we'll be able to relocate the chip under the hood by the 12V battery,
and route wires in a better way.

Binary file not shown.

View File

@ -0,0 +1,79 @@
---
title: Cars I Have Known
date: 2022-10-02T00:00:00Z
---
I've owned a few cars by now. In chronological order of ownership:
1. ~1972 Oldsmobile Delta 88
2. ~1972 Oldsmobile Toronado
3. ~1989 Honda Prelude
4. ~1998 Subaru Legacy Wagon
5. ~1985 Volkswagon Vanagon
6. 2007 Subaru Outback
7. 2016 Tesla Model S
8. 2019 Tesla Model 3
9. 2017 Nissan LEAF
10. 2019 Chevrolet Bolt
Tesla manages to be very much in the public conciousness.
There's this persistent story that the cars are poorly built and unreliable.
That hasn't been my experience.
Reliability
----------
This next list ranks these cars from most to least reliable,
including a need for repairs,
taking into account that gas cars have a higher baseline maintenance burden.
1. ~1989 Honda Prelude: Nothing
2. 2017 Nissan LEAF: Several recalls when we first bought it
3. Tesla Model 3: Recall on a cable in the trunk
4. 2019 Chevrolet Bolt: Software updates can't happen over the air, and a battery recall that was still open when we bought it
5. ~1998 Subaru Legacy Wagon: Alternator failure made car die over train tracks!
6. Tesla Model S: Three door handle microswitch failures, all covered by warranty
7. 2007 Subaru Outback: Head gasket failed
8. ~1972 Oldsmobile Delta 88: Older car needed lots of twiddling
9. ~1972 Oldsmobile Toronado: Older front wheel drive car needed lots and lots of twiddling
10. ~1988 Volkswagon Vanagon: An endless parade of mechanical problems
Overall ownership experience
--------------------------------
Now including regular maintenance items,
here are the top 5 from most to least pain in the butt to own:
1. Tesla Model 3: The only shop visit has been to replace tires
2. Tesla Model S: Multiple warranty repairs all happened in our driveway
3. 2017 Nissan LEAF: One shop trip to have multiple recalls performed
4. 2019 Chevrolet Bolt: One shop trip to have recalls performed
5. ~1989 Honda Prelude: Required regular maintenance at an auto shop
6. ~1998 Subaru Legacy Wagon: Regular maintenance at shop, and alternator replacement ate 3 days from our vacation and also required body work because it fell off the tow truck
Servce Center Experience
------------------------
And in terms of best to worst service center experience,
the list is:
1. Tesla
2. Everybody else
I could write pages and pages of complaints about the dealership service center experience
for every car we've had.
I'm really hoping,
now that Tesla has illustrated how to sell and service cars without dealerships,
that other manufacturers start doing the same thing,
because the dealership experience is uniformly horrid.
---
$SPOUSE, on reading this list,
commented that if she had to buy a gas car again,
she'd want something like the Legacy wagon.
I agree with that: it was a pretty good gas car.
But we'd rather have any electric than any gas car.
They need way less regular maintenance,
which means that even the nightmare experience of dealing with a dealership is less frequent.

View File

@ -0,0 +1,228 @@
---
title: CLRG's Cheating Scandal
date: 2022-10-04
tags:
- clrg
---
$SPOUSE just stumbled across a PowerPoint file with a bunch of text messages,
pretty clearly illustrating a massive cheating scandal.
I won't post a link to the file,
I won't post any screen shots,
and I won't reveal any names,
because I'm not interested in doxxing people.
Summary
------------
The PowerPoint file consists of text messages between someone who I will call
Sam and a number of CLRG school owners and adjudicators. Sam's messages
illustrate what appears to be a years-long, maybe decades-long, racket in which
adjudicators trade favors in exchange for inflated scores of chosen dancers.
These favors are either reciprocal, or, in one case, sexual.
I see weak evidence of money changing hands.
In addition, it is made clear that a school owner is having an extramarital
affair with Sam.
Technical Analysis
-------------------------
The PowerPoint document going around appears to be a series of screen shots from
the phone of an adjudicator who I will refer to as Sam. Sam appears to have
gone back through chat history, taken a bunch of screen shots, and emailed them
to someone whose name I don't recognize. The text of two emails is included in
the document before the screen shots which must have been attachments.
The icons at the top of the screen shots indicate that Sam has an Android
phone. Android allows you to take screen shots by pressing the power button and
the volume down button at the same time. It's common for people to hit the
volume button before power, which results in the phone turning the volume down
just before the screen shot is taken. You can see evidence of this: the volume
control appears in some of the screen shots. Someone more familiar with mobile
phones could tell you the Android version and phone manufacturer.
The screen shots are of text messages with WhatsApp and Instagram (thanks to my
daughter for figuring this one out). In both apps, Sam is writing the text on
the right side of the screen, and the person Sam is chatting with is on the left
side. That person's name and their profile icon (usually a photo of their head)
can be found at the top of the screen.
The icons make it fairly clear that these messages all came from one phone. The
main apps I see are Twitter, Spotify, something that's just a dot, and another
thing I don't recognize that looks sort of like a stylized four-leafed clover.
There isn't really too much more to analyze here,
from a technical standpoint.
My Take
-----------
I feel compelled to highlight a few observations:
### Sam is a man
Due to the sexual messages,
specifically one where Sam is called a "good boy",
and other clues leading us to Sam's real identity,
my team feels confident that Sam is a man.
### Sam might be creeping on kids
There's a weird message, from Sam, saying "178 is hot". There's no additional
context here, other than that 178 is a U11 girl, which means she is 11 years old
or younger.
It's possible this was just poor phrasing and "178 is on fire" would have been
better.
### Sam is involved in an affair
In the most memorable part of the document,
one school owner and Sam exchange messages outlining an extramarital affair.
It's not clear whether the school owner's spouse is aware of this.
But it is clear that the school owner likes Sam to have his "ass up".
### Sam didn't intend for this to become widespread
While I don't know what his motivation was for taking and sending these screen
shots, I feel sorry for this person. Nothing in this document makes it look like
Sam was trying to publicize what was going on. And there are a few things that
Sam surely regrets sending, such as the affair.
In fact, there are enough clues in these messages that my team is confident they
know Sam's real name. If we're right, Sam definitely would not want any of this
to become public.
### This is old
The earliest message is from July 2016.
### This is deep
In an exchange on slide 5, 6 of the 14 judges at All-Irelands 2019 are
implicated as colluding.
In an exchange on slide 3, it is implied that up to 5 adjudicators are not part
of the racket, or are at least not bribeable by the person who refers to them as
"the panel from hell". Let's be optimistic and assume that all 5 of these
adjudicators are not bribeable.
Taking both of these together, we can guess that 6 to 9 of the adjudicators at
All Irelands 2019 were in on this racket.
There's a lot of math to get into here: accounting for how the overall
rankings are computed may make it possible for two colluding adjudicators (16%
chance) to compromise the entire ranking. But starting out with half the judges
compromised belies severe problems, no matter how you slice it.
### This probably isn't as effective as you might think
Most of these messages are just lists of numbers.
One smart cheater sent a photograph,
which would be easier to remember.
The reality is likely that they don't remember all the numbers with 100% accuracy.
This would result either in fair judging,
or throwing points at the wrong number.
The younger dancers would have the most inaccuracy in cheating.
For the older competitors, the accuracy of cheating goes up,
as the adjudicators grow to recognize dancers.
Another thing that helps fairness is the number of adjudicators. At smaller
events, there are 3 on a panel, so about a 5% chance that everybody is
compromised, if half the judges are bad.
This is why there are more adjudicators at the higher-placing events.
### This could be detected
If anybody ever bothered to do analysis of the scores handed out, it would
be pretty easy to detect the type of cheating that this document seems
to illustrate. That wouldn't stop the cheating, but it would make it harder.
Unfortunately, I don't believe any of the software in use does anything like
this. And I don't think the data is made available in a way that would make
independent audits feasible.
The data exists to be analyzed and audited, but until there's a lot of pressure
to do this, I don't see it happening. And as angry as everyone is about things,
I doubt the result will be auditable data. Doing audits is expensive, and the
people with lots of money have more incentives to use that money to bribe adjudicators.
### These messages are just the tip of the iceberg
Let's do some arithmetic!
Including Sam,
there are 14 people implicated by this document.
We are just reading Sam's messages: presumably everyone involved is messaging
everyone else as well. Mathematicians call this a "complete graph". That means
there are `(14*13)/2 = 91` conversations. Put another way, we are only seeing 15%
of the racket within this group of 14 people
But these appear to mostly be UK people.
If this racket is larger than the UK,
you can expect much, much more has been going on that we have yet to learn about.
And you can bet that it's larger than the UK:
it is difficult to envsion how something like this could stay that regional,
with an organization as international as CLRG.
This doesn't mean your school is involved.
But it probably does mean your school has been affected.
Background
---------------
### CLRG and Competitive Irish Dance
An Coimisiún le Rincí Gaelacha (CLRG)
is the largest competitive Irish Dance organization in the world.
Their main activity,
as far as I know,
is running competitions, called "Feisanna",
which Americans pronounce like "feshes",
because we can't read Gaelic.
These events are adjudicated by certified judges,
most of whom also own dance schools of their own.
These judges fly out to the events,
sit through 8-12 hours of mostly children dancing around on a stage,
try to write helpful notes in just a few seconds,
assign a score,
and then move on to the next round.
Periodically, the scores are tabulated by volunteers
(mostly dads, from my experience),
entered into one of a handful of online systems just for Irish Dance competitions,
and awards are presented based on results computed by the systems.
(None of these systems have auditable source code, to my knowledge,
but that's a topic for another article.)
### About Me
I'm a computer security researcher and educator,
specializing in forensic investigation.
That means I show up after the bad thing has happened,
and build a story about what happened based on gathered evidence.
The analysis I'm doing here isn't particularly difficult:
my teenage daughter actually helped me put it together.
But I am coming at this from a history of doing similar things.
My daughter does competitive Irish Dance with CLRG.
She's been dancing for over 12 years now.
My involvement has mostly stayed peripheral:
running sound and electrical at events,
helping with tabulation,
and driving everybody home while they sleep.
I am not a journalist.
I'm really looking forward to reading what gets written about this
by somebody who does investigative journalism for a living.
But that article isn't out yet,
so I wrote this one.

View File

@ -0,0 +1,46 @@
---
title: My Take on CLRG's Cheating Scandal
date: 2022-10-05
tags:
- clrg
---
This was originally part of my
[technical analysis / explainer](2022-10-04-CLRG-cheating.html)
post,
but I've moved it here because it's just opinion.
---
Trees grow up *and* down. The part of the tree you can see is only half of the
organism: for every meter of Siberian Elm tree I can cut down, there's a meter
of Siberian Elm tree I need to dig out, and digging is way more work than
cutting.
I suspect that this racket is deep-rooted and widespread. I imagine the most
likely outcome will be a lot of social media posts, some funny T shirts, a few
resignations, maybe some people getting banned from events, and that's about it.
The only adjudicator this document proves is corrupt is the whisleblower who
sent in the screen shots. Even people named in these messages stand a decent
chance of escaping consequences, if they play their cards right.
Don't forget that you need a lot of certified adjudicators for things to run at
their current scale. They can't just fire everybody and start over: they need
their current instructors to train up new ones. And if this document is
representative, half of their current instructor pool is already compromised.
There's no way to ensure this scheme won't be a part of the new adjudicator
training.
Somebody is going to have to deliver a candid presentation to the board, or
whatever they have, about the reality here. The corruption is probably so deep
that they won't be able to take any significant action without destroying the
whole organization. And this is just going to embolden the bad actors.
After time passes, the presure from some of these intense and monied parents
I've met will resume, and everything will be back to the way it was before.
Sure, there will be some casting changes, but the system that's been built up
for years--possibly decades--is going to persist long after the furore dies
down.
We may, however, see a renewed interest in some of the smaller Irish Dance
organizations like CRN, who already seems to be making some moves.

View File

@ -0,0 +1,122 @@
let awardPoints = [
100, // 1
75, // 2
65, // 3
60, // 4
56, // 5
53, // 6
50, // 7
47, // 8
45, // 9
43, // 10
41, // 11
39, // 12
38, // 13
37, // 14
36, // 15
35, // 16
34, // 17
33, // 18
32, // 19
31, // 20
30, // 21
29, // 22
28, // 23
27, // 24
26, // 25
25, // 26
24, // 27
23, // 28
22, // 29
21, // 30
20, // 31
19, // 32
18, // 33
17, // 34
16, // 35
15, // 36
14, // 37
13, // 38
12, // 39
11, // 40
10, // 41
9, // 42
8, // 43
7, // 44
6, // 45
5, // 46
4, // 47
3, // 48
2, // 49
1, // 50
0.75, // 51
0.65, // 52
0.60, // 53
0.56, // 54
0.53, // 55
0.50, // 56
0.47, // 57
0.45, // 58
0.43, // 59
0.41, // 60
0.39, // 61
0.38, // 62
0.37, // 63
0.36, // 64
0.35, // 65
0.34, // 66
0.33, // 67
0.32, // 68
0.31, // 69
0.30, // 70
0.29, // 71
0.28, // 72
0.27, // 73
0.26, // 74
0.25, // 75
0.24, // 76
0.23, // 77
0.22, // 78
0.21, // 79
0.20, // 80
0.19, // 81
0.18, // 82
0.17, // 83
0.16, // 84
0.15, // 85
0.14, // 86
0.13, // 87
0.12, // 88
0.11, // 89
0.10, // 90
0.09, // 91
0.08, // 92
0.07, // 93
0.06, // 94
0.05, // 95
0.04, // 96
0.03, // 97
0.02, // 98
0.01, // 99
0.00, // 100
]
function init() {
for (let tbody of document.querySelectorAll(".awardPoints tbody")) {
for (let i = 0; i < awardPoints.length; i++) {
let tr = tbody.appendChild(document.createElement("tr"))
tr.appendChild(document.createElement("td")).textContent = i + 1
tr.appendChild(document.createElement("td")).textContent = awardPoints[i].toFixed(2)
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
export {
awardPoints,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,261 @@
---
title: CLRG Scoring Analyzed
date: 2022-10-09
tags:
- clrg
stylesheets:
- toys.css
scripts:
- scorecard.mjs
---
Let's take a look how how CLRG does its scoring!
*With math!*
## How CLRG Scoring Works
As I am given to understand, the scoring works like so:
1. Adjudicators give you a "raw score": a real number between 0 and 100
2. The scoring system ranks each dancer per adjudicator, based on raw scores
3. These rankings are mapped into "award points"
4. All of a dancer's award points are summed
5. Final ranking is determined by comparing total award points
## Raw Scoring
The way raw scores translate into rankings and award points is a little
confusing, so I've made a little tool you can play with to get a feel for how it
works. Essentially, it's a way of normalizing places to an adjudicator: score
weights are only relative to the judge that assigns them.
Adjudicator A can assign scores between 80 and 100;
adjudicator B can assign scores between 1 and 40;
and they'll both have a first, second, third, fourth place, etc.
These places then get translated into award points.
## Award Points
Award points are handed out based on ranking against other dancers for that
adjudicator. I obtained these values from a FeisWorx results page for my kid:
<div class="awardPoints">
<table>
<thead>
<tr>
<th>Ranking</th><th>Award Points</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
If there's a 2-way, 3-way, or n-way tie,
all tied dancers get the average of the next 2, 3, or n award points,
and the next 2, 3, or n rankings are skipped.
## What's with these values?
At first glance, the award points look like the output of an exponential function.
{{<figure src="chart.png" alt="Chart of scores vs. award points">}}
In an effort to figure out where these numbers came from,
I ran some curve fitting against the data.
Here's the best I could come up with:
| Ranking range | Award Points Function | Type of function |
| --: | --: | --- |
| 1 - 11 | 100 * x^-0.358 | Exponential |
| 12 - 50 | 51 - x | Linear |
| 51 - 60 | 14.2 - 0.46x + 0.00385x | Polynomial |
| 61 - 100 | 1 - x/100 | Linear |
If you, dear reader, are a mathematician,
I would love to hear your thoughts on why they went with this algorithm.
There are a few points to note here:
* 1st place is a *huge deal*. Disproportionately huge.
* Places 2-10 are similarly big deals compared to places 3-11.
* Places 12-50 operate the way most people probably assume ranking works: linearly.
* Places 51-60 fit best to a second degree polynomial, but it doesn't matter much for differences of hundreths of a point. This section is *really weird*, mathematically.
* Places 61-100 are all less than 1 point. If you're a judge trying to tank a top dancer, anywhere in this range is equivalent to anywhere else.
## Consequences of Exponential Award Points
Playing around with this,
I've found a few interesting consequences
of the exponential growth in the top 11 places.
### 1st place is super important
1st place is weighted so heavily that one judge could move a 5th place dancer into 2nd.
<table class="scorecard">
<thead>
<tr>
<td></td>
<th>Alice</th>
<th>Bob</th>
<th>Carol</th>
</tr>
</thead>
<tbody>
<tr>
<th class="justify-left">Adj. 1</th>
<td><input type="number" min=1 max=99 value=1 readonly></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
<tr>
<th class="justify-left">Adj. 2</th>
<td><input type="number" min=1 max=99 value=5></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=1></td>
</tr>
<tr>
<th class="justify-left">Adj. 3</th>
<td><input type="number" min=1 max=99 value=5></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="justify-left">Award Points</th>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
</tr>
<tr>
<th class="justify-left">Ranking</th>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
</tr>
</tfoot>
</table>
You can adjust these values to get a better feel for how scoring works.
### Tanking a high-ranked dancer is another way to cheat
Because of that exponential curve,
a low ranking from a single judge can carry a lot of weight.
<table class="scorecard">
<thead>
<tr>
<td></td>
<th>Alice</th>
<th>Bob</th>
<th>Carol</th>
</tr>
</thead>
<tbody>
<tr>
<th class="justify-left">Adj. 1</th>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=11></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
<tr>
<th class="justify-left">Adj. 2</th>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
<tr>
<th class="justify-left">Adj. 3</th>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=1></td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="justify-left">Award Points</th>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
</tr>
<tr>
<th class="justify-left">Ranking</th>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
</tr>
</tfoot>
</table>
### Being in 1st provides a nice buffer
Try playing around with Alice's rankings with Adjudicators 2 and 3 here.
She has to get ranked a lot lower before her overall ranking starts going down.
<div class="scrolly">
<table class="scorecard">
<thead>
<tr>
<td></td>
<th>Alice</th>
<th>Bob</th>
<th>Carol</th>
<th>Dave</th>
<th>Erin</th>
</tr>
</thead>
<tbody>
<tr>
<th class="justify-left">Adj. 1</th>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=4></td>
<td><input type="number" min=1 max=99 value=5></td>
</tr>
<tr>
<th class="justify-left">Adj. 2</th>
<td><input type="number" min=1 max=99 value=7></td>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=4></td>
</tr>
<tr>
<th class="justify-left">Adj. 3</th>
<td><input type="number" min=1 max=99 value=5></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=4></td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="justify-left">Award Points</th>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
</tr>
<tr>
<th class="justify-left">Ranking</th>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
</tr>
</tfoot>
</table>
</div>

View File

@ -0,0 +1,66 @@
import {awardPoints} from "./awardPoints.mjs"
function scorecardUpdate(scorecard) {
let scores = []
let points = []
let highestRank = []
let firstRow = scorecard.querySelector("tbody tr")
for (let input of firstRow.querySelectorAll("input")) {
scores.push(0)
points.push(0)
}
for (let row of scorecard.querySelectorAll("tbody tr")) {
let i = 0
for (let input of row.querySelectorAll("input")) {
let ranking = Number(input.value)
scores[i] += ranking
points[i] += awardPoints[ranking]
highestRank[i] = Math.min(highestRank[i] || 100, ranking)
i += 1
}
}
{
let i = 0
for (let out of scorecard.querySelectorAll("tfoot output[name='points']")) {
out.value = points[i]
i += 1
}
}
{
let rankOffset = 0
let overallRanking = []
let rankedPoints = [...points].sort((a, b) => b - a)
for (let i = 0; i < points.length; i++) {
overallRanking[i] = rankedPoints.indexOf(points[i]) + 1
if (overallRanking[i] == 1) {
rankOffset = highestRank[i]
}
}
let i = 0
for (let out of scorecard.querySelectorAll("tfoot output[name='ranking']")) {
out.value = rankedPoints.indexOf(points[i]) + rankOffset
i += 1
}
}
}
function init() {
for (let scorecard of document.querySelectorAll(".scorecard")) {
for (let input of scorecard.querySelectorAll("input")) {
input.addEventListener("input", () => scorecardUpdate(scorecard))
}
scorecardUpdate(scorecard)
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}

View File

@ -0,0 +1,33 @@
.warning {
color: #e64;
display: none;
}
.warning.visible {
display: initial;
}
.scrolly {
max-width: 100%;
overflow-x: auto;
}
.awardPoints {
display: inline-block;
max-height: 60vh;
overflow-y: auto;
margin: 1em;
}
.awardPoints table {
margin: initial;
}
.awardPoints thead {
position: sticky;
top: 0;
}
.awardPoints tbody {
max-height: 60vh;
overflow-y: auto;
}
.awardPoints td {
text-align: right;
}

View File

@ -0,0 +1,122 @@
let awardPoints = [
100, // 1
75, // 2
65, // 3
60, // 4
56, // 5
53, // 6
50, // 7
47, // 8
45, // 9
43, // 10
41, // 11
39, // 12
38, // 13
37, // 14
36, // 15
35, // 16
34, // 17
33, // 18
32, // 19
31, // 20
30, // 21
29, // 22
28, // 23
27, // 24
26, // 25
25, // 26
24, // 27
23, // 28
22, // 29
21, // 30
20, // 31
19, // 32
18, // 33
17, // 34
16, // 35
15, // 36
14, // 37
13, // 38
12, // 39
11, // 40
10, // 41
9, // 42
8, // 43
7, // 44
6, // 45
5, // 46
4, // 47
3, // 48
2, // 49
1, // 50
0.75, // 51
0.65, // 52
0.60, // 53
0.56, // 54
0.53, // 55
0.50, // 56
0.47, // 57
0.45, // 58
0.43, // 59
0.41, // 60
0.39, // 61
0.38, // 62
0.37, // 63
0.36, // 64
0.35, // 65
0.34, // 66
0.33, // 67
0.32, // 68
0.31, // 69
0.30, // 70
0.29, // 71
0.28, // 72
0.27, // 73
0.26, // 74
0.25, // 75
0.24, // 76
0.23, // 77
0.22, // 78
0.21, // 79
0.20, // 80
0.19, // 81
0.18, // 82
0.17, // 83
0.16, // 84
0.15, // 85
0.14, // 86
0.13, // 87
0.12, // 88
0.11, // 89
0.10, // 90
0.09, // 91
0.08, // 92
0.07, // 93
0.06, // 94
0.05, // 95
0.04, // 96
0.03, // 97
0.02, // 98
0.01, // 99
0.00, // 100
]
function init() {
for (let tbody of document.querySelectorAll(".awardPoints tbody")) {
for (let i = 0; i < awardPoints.length; i++) {
let tr = tbody.appendChild(document.createElement("tr"))
tr.appendChild(document.createElement("td")).textContent = i + 1
tr.appendChild(document.createElement("td")).textContent = awardPoints[i].toFixed(2)
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
export {
awardPoints,
}

View File

@ -0,0 +1,35 @@
---
title: CLRG Award Points Artifacts
date: 2022-10-10T08:00:00-06:00
tags:
- clrg
stylesheets:
- toys.css
scripts:
- speculator.mjs
---
One quirk of awards points is that for any given overall
score, there are only a handful of possible judge rankings that could have led
to it. That means you can make some guesses about how each judge ranked an
individual dancer, based on only their total award points.
Here's a handy calculator!
It (currently) doesn't consider the possibility of a tie.
<div class="scrolly">
<fieldset class="speculator">
<legend>CLRG Award Points Speculator</legend>
<div>
Points: <input name="points" type="number" min=41 max=10000 value=188>
<input name="adjudicators" type="hidden">
</div>
<table class="results">
<caption>Possible Rankings</caption>
<thead>
<tr class="warning"><th>Computing: this could take a while!</th></tr>
</thead>
<tbody></tbody>
</table>
</fieldset>
</div>

View File

@ -0,0 +1,104 @@
import { awardPoints } from "./awardPoints.mjs"
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
function awardPossibilities(total=0, depth=1) {
if (depth == 1) {
if (awardPoints.includes(total)) {
return [[total]]
} else {
return []
}
}
let possibilities = []
for (let p of awardPoints) {
if (p <= total) {
for (let subPossibility of awardPossibilities(total - p, depth - 1)) {
let v = [p].concat(subPossibility)
v.sort((a,b) => b-a)
possibilities.push(v)
}
}
}
possibilities.sort((a,b) => b.reduce((a,b) => a*100+b) - a.reduce((a,b) => a*100+b))
let uniquePossibilities = []
for (let p of possibilities) {
if (uniquePossibilities.length == 0 || !arraysEqual(p, uniquePossibilities[uniquePossibilities.length - 1])) {
uniquePossibilities.push(p)
}
}
return uniquePossibilities
}
function speculate(calc) {
let points = calc.querySelector("[name=points]").value || 0
let adjudicators = calc.querySelector("[name=adjudicators]").value || 3
let results = calc.querySelector(".results tbody")
while (results.firstChild) {
results.removeChild(results.firstChild)
}
for (let warning of calc.querySelectorAll(".warning")) {
if (adjudicators >3) {
warning.classList.add("visible")
setTimeout(() => asyncSpeculate(calc, points, adjudicators), 0)
} else {
warning.classList.remove("visible")
asyncSpeculate(calc, points, adjudicators)
}
}
}
async function asyncSpeculate(calc, points, adjudicators) {
let results = calc.querySelector(".results tbody")
let possibilites = awardPossibilities(points, adjudicators)
if (possibilites.length == 0) {
let row = results.appendChild(document.createElement("tr"))
let cell = row.appendChild(document.createElement("th"))
cell.textContent = "No possible combinations"
} else {
let row = results.appendChild(document.createElement("tr"))
for (let i = 1; i <= adjudicators; ++i) {
let cell = row.appendChild(document.createElement("th"))
cell.textContent = "Adj. " + i
}
for (let possibility of possibilites) {
let row = results.appendChild(document.createElement("tr"))
for (let p of possibility) {
let cell = row.appendChild(document.createElement("td"))
cell.textContent = p
}
}
}
for (let warning of calc.querySelectorAll(".warning")) {
warning.classList.remove("visible")
}
}
function init() {
for (let calc of document.querySelectorAll(".speculator")) {
for (let input of calc.querySelectorAll("input")) {
input.addEventListener("input", () => speculate(calc))
}
speculate(calc)
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}

View File

@ -0,0 +1,33 @@
.warning {
color: #e64;
display: none;
}
.warning.visible {
display: initial;
}
.scrolly {
max-width: 100%;
overflow-x: auto;
}
.awardPoints {
display: inline-block;
max-height: 60vh;
overflow-y: auto;
margin: 1em;
}
.awardPoints table {
margin: initial;
}
.awardPoints thead {
position: sticky;
top: 0;
}
.awardPoints tbody {
max-height: 60vh;
overflow-y: auto;
}
.awardPoints td {
text-align: right;
}

View File

@ -0,0 +1,2 @@
# I don't have rights to copy any of the data :(
*.xml

View File

@ -0,0 +1,159 @@
---
title: CLRG Results Analysis
date: 2022-10-28T10:45:00-0600
tags:
- clrg
---
# Our Findings
Here's a summary of what the team I've been working with has found:
## It's clearly widespread throughout the organization
It's more than 12 people.
It's more than 24 people.
It's probably more than 48 people.
Every data set we found had some pretty clear weirdness,
and that was just looking at numbers.
Once we tied names back in to weirdness,
we were like, "oh, yeah, we had a feeling this person was up to something."
Unless there are some *major* changes made,
we're still going to have corruption in CLRG.
(Spoiler alert: there will not me major changes made.)
That's just the world you're in with CLRG,
and most other subjectively-judged competitive events.
I hope any new families getting involved understand this:
In order to get into the upper tiers,
the way you compete becomes more about politics than dancing.
And by "politics" I mean corruption.
That's not to say the dancers at the upper levels aren't excellent dancers: to
recall at a national event, you have to be an excellent dancer. But you might
also be an excellent dancer and not recall, because your parents/coach/teacher
aren't playing the corruption game as well as somebody else's
parents/coach/teacher.
## The top 11 places are bizarre
We knew this from [previous analysis](/blog/2022-10-09-CLRG-Scoring.html):
the top 11 places are scored totally differently than places 12-50.
And places 51-100 are placed separately.
Any large event is actually three separate competitions,
and it's very very difficult to break out if any judge places you in one of these categories:
| Placing | Comment |
| ---- | ---- |
| 1st - 11th | Strange exponential points category |
| 12th - 50th | Scoring here works the way you assumed it would |
| 51st - 100th | Everybody's fighting for a fraction of one point |
Please note that this is just my hot take!
You should play with the
[scoring tool](/blog/2022-10-09-CLRG-Scoring.html) I made
to get a feel for how this all works. It's weird!
And it's difficult enough to explain accurately by someone trying to.
I'm not trying to in this section.
# I won't publish any more tools
I started writing a thing to highlight weirdness in CLRG rankings.
You'd give it a ranking sheet,
and it would highlight what weirdness it found,
with an explanation about why it looks weird and what it might mean.
But I gave up after a day's work.
Here's why:
## I don't have the right to copy data
The results of competitions is owned by various companies. It seems to be a
different company depending on who gets the contract to provide the scoring
software for a particular event. In any case, none of them provide a license
that allows me to redistribute their data. That means I can't host any scores on
this web site: you have to get it from the company that owns it.
## The data is distributed as PDF files
Adobe Acrobat (or whatever they call it now) actually has an "export as XML"
function that does a good job turning PDF files back into something like a
spreadsheet.
In order for any tool I make to be generally useful, I would also need to
provide instructions on doing that Acrobat export, probably with an accompanying
video and multiple screen shots. I don't even run Windows or Mac OS, to say
nothing of being notoriously bad at this sort of instructional page / video.
## It's not clear anybody really cares
Reading the "voy forums", it's clear that the main thing people are getting out
of this is righteous indignation. I don't think a post full of math would really
appeal to the people there.
I'm in touch with a couple of reporters covering this story, but I don't think
the math angle is going to be very interesting to their readership either.
That means I'd need to go and try to figure out who *does* care. I found a small
group of people who care, and this group has already loaded some data into a
spreadsheet and done a manual analysis.
After finding mathematical evidence supporting what we already knew (this whole
process is corrupt), what then? I guess I just go on with my life.
I can already just go on with my life, I don't have to put in a bunch of work first.
# Parting thoughts
My kid is a high school senior.
She has a lot of things to look forward to in her immediate future that aren't Irish Dance,
and is winding down her involvement,
so our family is sort of meh about this whole thing.
If she were in elementary or middle school,
I would probably be howling right now and pressing hard to pull her out.
But maybe there's some value to still doing all this,
even though it's corrupt and she's never going to get a top ranking.
She still wants to compete for some reason,
and practicing has helped her develop a work ethic that will help her later.
In addition,
she's made friends through this;
she's learned how to care for others and talk to new people;
she's learned how to teach;
and she's gained a strong sense of self.
Those are all good things that didn't depend on fair judging.
The other day I was talking with a woman who runs an after-school program for
black kids who are interested in science and technology. I mentioned that the
winning papers at the statewide computer science contest never seem to integrate
the social justice aspects she's asking her kids to focus on. We kicked that
idea around a while, and wound up convincing each other that the work is worth
doing even if the judging is biased against it. I think the same thing might be
true here.
# Do you care?
Are you a regular reader of my blog? (HA HA HA) Do you care about mathematical
analysis of this stuff? Are you willing to jump through some technical hoops in
order to look at things without running afoul of copyright law? Get in touch
with me and let me know there's actually an audience!
All of the code I wrote is checked in to git for this blog page.
So you don't even need to contact me,
you can just take the scraping code and go nuts.
It uses a standard API for scraped data from two different sources,
does some smarts to determine missing data,
and should be pretty simple to interface with.
If you need help getting the XML data into it,
I'd be glad to help you with that.
Here are the files:
* [Feisworx report scraping code](feisworx.mjs)
* [Feis Results report scraping code](feisresults.mjs)
* [Code to guess placing given award points, used by feisresults.mjs](awardpoints.mjs)
* [JSDoc documentation of some global data structures](types.mjs)
* [Some stub code to populate an HTML page with data](dataset.mjs)

View File

@ -0,0 +1,25 @@
---
title: Unfinished CLRG Data Analyzer
stylesheets:
- dataset.css
scripts:
- dataset.mjs
---
<p>
This won't work because you don't have the datasets.
I can't provide them to you, due to copyright laws.
But if you get the results PDFs,
load them up in Adobe Acrobat,
and save them as XML,
they might load here :)
</p>
<h1>2021 Irish Dance North Americans 21A</h1>
<div class="clrg-dataset" data-url="2022-10-10 2021 Irish dance north Americans 21A.xml"></div>
<h1>2017 11 AB Wro</h1>
<div class="clrg-dataset" data-url="2017-11 AB Wro.xml"></div>
<h1>2019 09 Wro</h1>
<div class="clrg-dataset" data-url="2022-10-10 Wro2019-09.xml"></div>

View File

@ -0,0 +1,131 @@
let awardPoints = [
100, // 1
75, // 2
65, // 3
60, // 4
56, // 5
53, // 6
50, // 7
47, // 8
45, // 9
43, // 10
41, // 11
39, // 12
38, // 13
37, // 14
36, // 15
35, // 16
34, // 17
33, // 18
32, // 19
31, // 20
30, // 21
29, // 22
28, // 23
27, // 24
26, // 25
25, // 26
24, // 27
23, // 28
22, // 29
21, // 30
20, // 31
19, // 32
18, // 33
17, // 34
16, // 35
15, // 36
14, // 37
13, // 38
12, // 39
11, // 40
10, // 41
9, // 42
8, // 43
7, // 44
6, // 45
5, // 46
4, // 47
3, // 48
2, // 49
1, // 50
0.75, // 51
0.65, // 52
0.60, // 53
0.56, // 54
0.53, // 55
0.50, // 56
0.47, // 57
0.45, // 58
0.43, // 59
0.41, // 60
0.39, // 61
0.38, // 62
0.37, // 63
0.36, // 64
0.35, // 65
0.34, // 66
0.33, // 67
0.32, // 68
0.31, // 69
0.30, // 70
0.29, // 71
0.28, // 72
0.27, // 73
0.26, // 74
0.25, // 75
0.24, // 76
0.23, // 77
0.22, // 78
0.21, // 79
0.20, // 80
0.19, // 81
0.18, // 82
0.17, // 83
0.16, // 84
0.15, // 85
0.14, // 86
0.13, // 87
0.12, // 88
0.11, // 89
0.10, // 90
0.09, // 91
0.08, // 92
0.07, // 93
0.06, // 94
0.05, // 95
0.04, // 96
0.03, // 97
0.02, // 98
0.01, // 99
0.00, // 100
]
/**
* Given a score, calculate what placings could have gotten it.
*
* @param {Number} score Score we're going to guess
* @param {Number} tied Highest number n-way tie to consider
* @returns {Array.<Number>} List of possible placings
*/
function guessPlacing(score, tied=3) {
let placings = []
for (let t = tied; t > 0; t--) {
let totalPoints = score * t
for (let placing = 0; placing < awardPoints.length - t; placing++) {
let acc = 0
for (let i = 0; i < t; i++) {
acc += awardPoints[placing+i]
}
if (acc == totalPoints) {
placings.push(placing+1)
}
}
}
return placings
}
export {
awardPoints,
guessPlacing,
}

View File

@ -0,0 +1,12 @@
.clrg-dataset {
max-width: 100%;
overflow-x: auto;
}
.clrg-dataset tbody td.new-adjudication {
border-left: thin solid black;
}
.clrg-dataset tbody td.new-round {
border-left: thick solid black;
}

View File

@ -0,0 +1,180 @@
/**
* Feis Dataset Importer
*/
import * as FeisWorx from "./feisworx.mjs"
import * as FeisResults from "./feisresults.mjs"
/**
* @typedef {import("./types.mjs").Results} Results
* @typedef {import("./types.mjs").Result} Result
* @typedef {import("./types.mjs").Round} Round
* @typedef {import("./types.mjs").Adjudication} Adjudication
* @typedef {Array.<Array.<String>>} RawData
*/
/**
* Creates a new element and appends it to parent
*
* @param {Element} parent Element to append to
* @param {String} type Type of element to create
* @param {Object} [dataset] Data fields to set
* @returns {Element}
*/
function newElement(parent, type, dataset={}) {
let child = parent.appendChild(document.createElement(type))
for (let k in dataset) {
child.dataset[k] = dataset[k]
}
return child
}
/**
* Load a file and parse it into Results.
*
* @param {URL|String} url Location of file to load
* @returns {Results} Parsed results
*/
async function loadData(url) {
let resp = await fetch(url)
let contentType = resp.headers.get("Content-Type")
if (! contentType.includes("/xml")) {
console.error(`Cannot load data with content-type ${contentType}`)
return
}
let text = await resp.text()
let doc = new DOMParser().parseFromString(text, "text/xml")
let rawData = parseXMLDocument(doc)
return parseRawData(rawData)
}
/**
* Parse an XML document of feis results into a 2D array of strings
*
* @param {Document} doc XML Document
* @returns {RawData} Raw data
*/
function parseXMLDocument(doc) {
let table = doc.querySelector("Table")
let rawData = []
for (let dataRow of table.children) {
if (! ["tr"].includes(dataRow.tagName.toLowerCase())) {
console.warn(`Warning: unexpected XML tag ${dataRow.tagName}, expecting tr`)
continue
}
let row = []
for (let dataCell of dataRow.children) {
if (! ["th", "td"].includes(dataCell.tagName.toLowerCase())) {
console.warn(`Warning: unexpected XML tag ${dataRow.tagName}, expecting th/td`)
continue
}
row.push(dataCell.textContent)
}
rawData.push(row)
}
return rawData
}
/**
* Parse raw data into a list of adjudicators and results
*
* @param {RawData} rawData Raw data
* @returns {Results} Parsed Results
*/
function parseRawData(rawData) {
let firstRow = rawData[0]
if (firstRow[0].trim().toLowerCase() == "place awd pts") {
return FeisWorx.parse(rawData)
}
if (firstRow[firstRow.length-1].trim().toLowerCase() == "total ip *") {
return FeisResults.parse(rawData)
}
console.error("First row doesn't resemble anything I can cope with", firstRow)
}
/**
*
* Fills a table element with some results
*
* @param {Element} table Table to fill in
* @param {Results} results Results to fill with
*/
function fillTable(table, results) {
let head = newElement(table, "thead")
let row0 = newElement(head, "tr")
let row1 = newElement(head, "tr")
let row2 = newElement(head, "tr")
newElement(row0, "th").colSpan = 2
newElement(row1, "th").colSpan = 2
newElement(row2, "th").textContent = "Name"
newElement(row2, "th").textContent = "Rank"
let roundNumber = 0
for (let round of results[0].rounds) {
let roundCell = newElement(row0, "th")
roundCell.textContent = `Round ${++roundNumber}`
roundCell.colSpan = 3*round.length
for (let adjudication of round) {
let adjudicator = adjudication.adjudicator
let cell = newElement(row1, "th")
cell.textContent = adjudicator
cell.colSpan = 3
newElement(row2, "th").textContent = "Raw"
newElement(row2, "th").textContent = "Placing"
newElement(row2, "th").textContent = "Points"
}
}
let body = newElement(table, "tbody")
for (let result of results) {
let row = newElement(body, "tr")
newElement(row, "th").textContent = result.name
newElement(row, "th").textContent = result.overallRank
let i = 0
for (let round of result.rounds) {
let first = true
for (let adjudication of round) {
let raw = newElement(row, "td")
raw.textContent = adjudication.raw
raw.classList.add("new-adjudication")
if (first) {
raw.classList.add("new-round")
first = false
}
newElement(row, "td").textContent = adjudication.placing
newElement(row, "td").textContent = adjudication.points
i++
}
}
}
}
async function init() {
for (let div of document.querySelectorAll(".clrg-dataset")) {
let results = await loadData(div.dataset.url)
let table = newElement(div, "table")
fillTable(table, results)
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
export {
loadData,
parseXMLDocument,
parseRawData,
}

View File

@ -0,0 +1,139 @@
/**
* Feisresults.com parser
*
*/
import {awardPoints, guessPlacing} from "./awardPoints.mjs"
/**
* @typedef {import("./types.mjs").Results} Results
* @typedef {import("./types.mjs").Result} Result
* @typedef {import("./types.mjs").Round} Round
* @typedef {import("./types.mjs").Adjudication} Adjudication
*/
/**
* Parse feisresults data
*
* @param {Array.<Array.<String>>} rawData Raw data
* @returns {Results}
*/
function parse(rawData) {
/** @type {Results} */
let results = []
let adjudicators = []
let numRounds = 0
let adjudicatorsPerRound = 0
let possibleTiesByAdjudicatorRound = {}
for (let rowIndex = 0; rowIndex < rawData.length; rowIndex++) {
let cells = rawData[rowIndex]
// Is it a page heading?
if ((cells[0].trim().toLowerCase() == "card")) {
continue
}
// Is it a list of adjudicators?
if (cells[cells.length-1].trim().toLowerCase() == "total ip *") {
cells.splice(cells.length-1, 1) // -1: total IP *
cells.splice(0, 5) // 0 - 4: blank
adjudicators = []
for (let cell of cells) {
cell = cell.trim()
if (cell.toLowerCase().includes("rounds 1")) {
// skip it
} else if (cell.toLowerCase().includes("round total")) {
numRounds++
} else {
adjudicators.push(cell)
}
}
adjudicatorsPerRound = adjudicators.length / numRounds
if (! Number.isSafeInteger(adjudicatorsPerRound)) {
console.error(`Irrational number of adjudicators for number of rounds: (${adjudicators.length}/${numRounds})`)
}
continue
}
let row = {}
row.number = Number(cells[0])
// cells[1]: Position at recall
row.overallRank = Number(cells[2])
{
let parts = cells[3].trim().split(/\s:\s/)
console.log(parts, cells[3])
let nameSchool = parts[0]
// parts[1]: region
// We're going to take a wild-ass guess here that the dancer only has two names
let subparts = nameSchool.split(/\s+/)
row.name = subparts.slice(0, 2).join(" ")
row.school = subparts.slice(2).join(" ")
}
row.qualifier = cells[4].trim()
/** @type {Round} */
let round = []
/** @type {Array.<Round>} */
row.rounds = []
let adjudicatorNumber = 0
for (let cellIndex = 5; cellIndex < cells.length; cellIndex++) {
let cell = cells[cellIndex].trim()
if (! cell.includes("/")) {
continue
}
/** @type {Adjudication} */
let adjudication = {}
adjudication.adjudicator = adjudicators[adjudicatorNumber++]
let parts = cell.split("/")
adjudication.raw = Number(parts[0])
adjudication.points = Number(parts[1])
adjudication.placing = guessPlacing(adjudication.points)
// Guidebook reports don't list every dancer: we'll guess placing later
round.push(adjudication)
if (round.length == adjudicatorsPerRound) {
row.rounds.push(round)
round = []
}
}
results.push(row)
}
disambiguatePlacings(results, numRounds, adjudicatorsPerRound)
return results
}
function disambiguatePlacings(results, numRounds, adjudicatorsPerRound) {
for (let roundNumber = 0; roundNumber < numRounds; roundNumber++) {
/**
* A list of raw score, award points, and placing
*
* @type {Array.<Adjudication>}
*/
for (let judgeNumber = 0; judgeNumber < adjudicatorsPerRound; judgeNumber++) {
let scores = []
for (let result of results) {
scores.push(result.rounds[roundNumber][judgeNumber])
}
scores.sort((a,b) => b.raw - a.raw)
let greatestPlacing = 0
for (let adjudication of scores) {
let possibilities = guessPlacing(adjudication.points)
possibilities.sort((a,b) => b-a)
// XXX: eliminate possibilities less than greatestPlacing, then pick the largest
}
console.log(scores)
}
}
}
export {
parse,
}

View File

@ -0,0 +1,153 @@
/**
* FeisWorx parser
*
* This is the output of Adobe Reader saving the PDF as XML.
*/
/**
* @typedef {import("./types.mjs").Results} Results
* @typedef {import("./types.mjs").Result} Result
* @typedef {import("./types.mjs").Round} Round
* @typedef {import("./types.mjs").Adjudication} Adjudication
*/
/**
* Parse FeisWorx data
*
* @param {Array.<Array.<String>>} rawData Raw data
* @returns {Results}
*/
function parse(rawData) {
/** @type {Results} */
let results = []
let adjudicators = []
let numRounds = 0
let adjudicatorsPerRound = 0
for (let rowIndex = 0; rowIndex < rawData.length; rowIndex++) {
let cells = rawData[rowIndex]
// Is it a page heading?
if ((cells.length >= 11) && (cells[0].trim().toLowerCase().startsWith("place"))) {
if (numRounds == 0) {
for (let cell of cells) {
if (cell.toLowerCase().startsWith("round")) {
numRounds++
}
}
}
continue
}
if (adjudicators.length == 0) {
let fishy = false
for (let adjudicator of cells) {
if (Number(adjudicator) > 0) {
fishy = true
}
adjudicators.push(adjudicator.trim())
}
if (fishy) {
console.warn("Adjudicators row doesn't look right", cells)
}
adjudicatorsPerRound = adjudicators.length / numRounds
if (! Number.isSafeInteger(adjudicatorsPerRound)) {
console.error(`Irrational number of adjudicators for number of rounds: (${adjudicators.length}/${numRounds})`)
}
continue
}
// Is this just a list of adjudicators again?
if (cells.length >= adjudicators.length) {
let lenDiff = cells.length - adjudicators.length
let same = true
for (let i = adjudicators.length-1; i >= 0; i--) {
if (adjudicators[i] != cells[i+lenDiff].trim()) {
same = false
break
}
}
if (same) {
continue
}
}
let row = {}
{
let parts = cells[0].trim().split(/\s+/)
row.overallRank = Number(parts[0])
row.overallPoints = Number(parts[1])
}
{
let match = cells[1].trim().match(/(\d+) - (.+) \((.+) *\)[ -]*(.+)?/)
if (match) {
row.number = Number(match[1])
row.name = match[2]
row.school = match[3]
row.qualifier = match[4]
}
}
/** @type {Round} */
let round = []
/** @type {Array.<Round>} */
row.rounds = []
for (let cellIndex = 2; cellIndex < cells.length; cellIndex++) {
let cell = cells[cellIndex]
/** @type {Adjudication} */
let adjudication = {}
let parts = cell.trim().split(/ - ?|\s/)
adjudication.adjudicator = adjudicators[cellIndex - 2]
if ((parts.length == 5) && (parts[3] == "AP")) {
parts.splice(3, 0, "NaN")
}
if ((parts.length == 7) && (parts[4] == "T")) {
adjudication.tie = true
parts.splice(4, 1)
} else {
adjudication.tie = false
}
if (parts.length != 6) {
console.error(`Wrong number of fields in row ${rowIndex} cell ${cellIndex}:`, parts, cells)
break
}
for (let i = 0; i < parts.length; i += 2) {
let key = parts[i]
let val = Number(parts[i+1])
switch (key) {
case "Raw":
adjudication.raw = val
break
case "Plc":
adjudication.placing = val
break
case "AP":
adjudication.points = val
break
default:
console.error(`Unknown key ${key} in row ${rowIndex} cell ${cellIndex}:`, cell)
break
}
}
round.push(adjudication)
if (round.length == adjudicatorsPerRound) {
row.rounds.push(round)
round = []
}
}
results.push(row)
}
return results
}
export {
parse
}

View File

@ -0,0 +1,34 @@
/**
* A collection of results
* @typedef {Array.<Result>} Results
*/
/**
* A single result
*
* @typedef {Object} Result
* @property {String} name Competitor's name
* @property {Number} number Competitor's bib number
* @property {String} school Competitor's school
* @property {Number} overallPoints Overall award points for this competitor
* @property {Number} overallRank Overall ranking for this competitor
* @property {String} qualifier Any qualifiers this ranking earned
* @property {Array.<Round>} rounds How this competitor was judged in each round
*/
/**
* The results for one dancer for one round
*
* @typedef Round
* @type {Array.<Adjudication>}
*/
/**
* One adjudicator's results for one dancer for one round
* @typedef {Object} Adjudication
* @property {String} adjudicator Adjudicator who recorded this score
* @property {Number} raw Raw score
* @property {Number} placing Placing relative to this adjudicator's other scores
* @property {Number} points Award points
* @property {Boolean} tie Whether this score was a tie
*/

View File

@ -0,0 +1,38 @@
---
title: My technological flag in the sand
date: 2022-10-28T14:59:00-0600
---
Elon Musk just bought Twitter. It got me thinking about Twitter, something that
comes up a lot, because a lot happens there in 2022, and some of that filters
through to me.
I've long known that the day would come when I would just not be willing to
accept some sort of societal change, and this would be what defined me as an old
person. This isn't some unique thing I'd be doing: this is a time-honored human
trait. Previous generations have put their flags in the sand by rejecting polyphonic
sacred music, rejecting automobiles, rejecting email, and rejecting smartphones.
Today I realized what my flag in the sand is going to be. I am not going to
become active on Twitter, or Instagram, or Facebook, or Myspace, or LiveJournal.
I see no personal benefit to adopting the newer technologies, and I see a lot of
personal benefits to avoiding them.
I'm not doing this because I think it's just a fad that humanity will move past.
No: now that we have it, and are beginning to realize how it actually works,
we're having to figure out how to use it responsibly as a species. This is a
pretty familiar story for any new technology. I'm just deciding I'm not
interested in being a part of that process.
At some point, some related technology will come along and sweep me up with it.
It will be awkward for me, and young people will recognize that I'm just no good
at it. They'll wonder how any human being could be so inept at something so
obvious. It will become a mental shortcut for the elderly that they are awkward
with this technology, and the elderly will discuss how pointless it is and how
they just can't understand why anyone needs it. We'll watch with some degree of
sadness as our way of life dies with us.
When you hear writers and philosophers talking about how limited lifespans are a
gift to humanity, they're talking about this sort of thing.
I hope y'all have a nice time :)

View File

@ -0,0 +1,52 @@
---
title: Smart Watches
date: 2022-11-28
---
My employer has this add-on to the health plan where,
if you wear a pedometer,
you get money in your health spending account.
If you also provide sleep tracking,
you get even more money.
It doesn't seem to care (currently) about your pulse.
I don't have any huge issue with this plan.
But I'd like to, as much as possible,
have this not be at the foreground of my day-to-day life.
After a lot of thinking,
I wound up realizing that the main consideration for me was battery life!
Believe it or not, all the other doodads and gizmos weren't actually that important.
So my requirements list is basically:
1. Tells me the time
2. Can go for a long time without needing to be recharged
3. Counts my steps and automatically puts them in this health plan thing
4. Also tell the health plan how long I sleep
I wound up going back to a watch I had a few years ago,
which has a month-long battery life between recharges.
If you turn off the thing where it vibrates when you get a phone call,
I think it will go even longer.
It also has the advantage of telling me the time the old-fashioned way:
with hands.
So I can just glance at it and see what time it is,
without needing to flick my wrist or get into a dark enough area or whatever.
I started writing this article in November,
and I'm finishing it in January.
I've been wearing that watch for a couple months now and it's no big deal.
It feels kind of silly writing about it.
But this is something a lot of people spend time and money thinking about in 2022!
Update: 2023-Sep-25
---------------
Since switching jobs, I've been wearing a Casio F-91W. I got it for $7 about 10 years ago.
It tells me the time, date, and weekday. It has an alarm, a stopwatch, and a barely-usable
light. It's been running on the battery it came with since I got it.
Every 4 years you have to tell it about leap year.
Apparently, my favorite watch is one I only have to think about every 4 years.

View File

@ -0,0 +1,78 @@
---
title: Iceland trip
date: 2023-01-06
---
We went to Iceland for two weeks over the winter break.
I couldn't think of anything I wanted to write in this blog about it,
but someone on somethingawful just asked me for some tips,
and my reply seems kinda interesting,
so here it is:
---
> Hey, really random, but saw you mention in the EV thread you just got back
> from Iceland, got any winter travel tips? I'm going in a few weeks, and I
> think pretty much everything I've read is focused on visiting in the summer.
I overpacked because I wasn't sure what to expect, but I didn't overpack by
much. I brought:
* A pair of insulated water-resistant pants (lined with fleece I think, sort of
like ski pants). Wore them everywhere.
* Two pairs of wool socks
* Waterproof boots: mine were Danner, my wife had Sorel
* two long-sleeved wool t-shirts
* two wool sweaters
* A waterproof parka with a hood
* A wool hat
* Warm waterproof gloves with the thing so you can still use the phone
touchscreen (this was so great to have)
I had planned to buy one of those Icelandic wool sweaters when I landed, which I
did, so I could have done without the second sweater I brought. The Icelandic
one was crazy warm: I found myself stripping off the coat and sometimes the
sweater while driving, to prevent overheating.
We only did little walks, like a short one to take a photo of Sólheimjökull, a
little one through Þingvellir. And lots of walking through snow in Reykjavik and
other rural places we stayed. The stuff we packed was totally adequate. I did
use the coat's hood way more than I thought I would, and was glad to have it. El
Cheapo crampons like YakTrax would have been a good idea for Þingvellir if we'd
wanted to go further, but once we saw the sign saying "if you get stuck past
here in the winter, we won't come help you", we turned around and found another
less-icy path. We weren't prepared for glacier climbing on a nature trail.
We traveled with another family, who had the smart idea to buy food at the
grocery store and prepare meals. That saved us a ton of money and I would highly
recommend doing it. Eating out was incredibly expensive. Groceries were also
expensive but more manageable.
For driving, just know your limits. They have these pullouts on the roads,
marked with a sign and an indication of how long the pullout is. If you are even
idly considering turning around, take the pullout and have a nice calm think
through that. I wound up seeing one, thinking "hmm, maybe I should turn around",
and then doing a K-turn on the freaking highway during a whiteout blizzard.
Nothing bad happened, thank goodness, but it could have gone very badly for me,
and I wished I had just taken a couple pullouts to discuss with my wife whether
we wanted to keep going. So take those pullouts.
Snow on the road is unlike anything I've ever witnessed in the Rocky Mountains,
and when they say "difficult driving conditions" in safetravel.is, pay
attention. It's not "if you're from California you'll have a hard time with
this", it's "if you're from any other country". We quickly learned to respect
their suggestions.
Lastly, the N1 fuel pumps wanted a PIN on my credit card, which I didn't have. I
think I could have used an ATM card, but instead we just asked the other family
to buy fuel for us, and we paid them back when we got home. Maybe bring an ATM
card and know the PIN! There were other gas stations where I could use Google
Pay on my phone, or Visa's tap-pay thing, with no problems. And inside the N1, I
could also use Google Pay. It was really just the pumps at the N1 stations.
Incidentally, you should pop into an N1 if you pass by one. It's a real trip.
You can even plan to have a meal there: I saw hot bars, salad bars, frozen
yogurt bars, nice custom sandwich things...
Overall the trip was great! We especially liked the westernfjörd area. The
south, near Sólheimjökull, was super touristy and I wouldn't go there on a
subsequent trip. I'm glad we got to go, and I hope you have a great time!

View File

@ -0,0 +1,25 @@
---
date: 2023-01-07
title: The Yurt
---
One day,
back in the late 1990s,
I discovered that the content filter appliance at work
had blocked woozle.org
for hosting porn.
Because I worked in the group that ran that filter,
I looked into what triggered this block.
I was pretty sure I didn't have any porn.
The offending image was called `yurt.png`.
It was a cartoon drawing of a yurt.
I told the filter it was a "false positive":
it wasn't actually porn.
That got the entire site unblocked.
I don't know why I put that image on my web server,
but the yurt has stayed on woozle.org ever since.
![A cartoon yurt](/assets/images/yurt.png)

View File

@ -0,0 +1,55 @@
---
date: 2023-01-12
tags:
- dr-who
- tv
title: 'Doctor Who S04E15: Dr Who goes to Scotland'
---
I skipped some episodes in the last year.
I think there were Cybermen.
There were definitely Daleks.
## S04E15: The Highlanders (1)
Okay I guess they're in Scotland.
This is another one of those episodes where all they could recover was
a few still frames and the audio,
so this episode is basically a slideshow.
I'm finding it difficult to care about this,
although this new doctor has a funny hat,
which is cute.
A lot of people are grunting in Scottish.
And there are bagpipes.
Did I mention I'm finding it difficult to care?
I may be about to decide to skip all the Earth episodes.
And yet I'm somehow halfway through the episode already.
The plot so far appears to be that the English and Scottish are fighting.
Some dudes named Grey and Perkins appear to be trying to do something nefarious.
I guess one of the new companions is named Polly.
She's going to throw a rock at the British in an attempt to stop a hanging.
Oh, are they hanging the other companion?
I think the Doctor is pretending to be some official or other again.
Grey and Perkins show up and stop the hanging,
looks like they're scavenging prisoners.
The doctor cites some law to Grey, a Lawyer, and manages to not be hanged.
The women (Polly and a Scottish woman)
are in a cave now.
I stopped paying attention.
The women were saying it was dark.
Then somebody screamed,
and credits rolled.
I guess we're still in the "women screaming" era that Susan started.
I am trying really hard but I just don't care.
I'm sorry.
I have a lot of things I could be doing with my time right now,
and almost all of them are more interesting than this.

View File

@ -0,0 +1,281 @@
---
date: 2023-01-24
title: "The Love Boat s02e01-e09"
tags:
- love-boat
- tv
---
My sister-in-law gave me a bunch of episodes of The Love Boat for some reason, so I'm watching them.
# Season 02, Episodes 01-02
The crew takes a trip to a deserted island only to discover it's inhabited by crazy
Gomez Addams (The Addams Family),
who takes them hostage at gunpoint and forces them to plan a surprise birthday party. They make plans to escape, but are thwarted by the arrival of a hurricane. An old lady inexplicably grows affection for the captor, and by the end of the double-episode plans the surprise party, then decides to stay behind and keep him company as she lives out the last few months of her terminal illness.
Meanwhile, Isaac the bartender manages to pull the wool over the eyes of a sexy model by having the crew talk him up like he's some rich successful dude. When she finds out he's the ship's bartender, she's angry, but, because she's a woman and therefore flighty, comes around to forgiving him after he takes responsibility for the safety of the passengers when the idiot acting captain doesn't. A love connection happens.
Finally, a woman who is a total jackass to her husband, during the hurricane on the island, confesses to him that the romance is gone, and she wants a real man. Her husband lowers his voice and makes up some stories about sleeping with a whole lot of women before he met her, and this somehow rekindles the romantic spark in their marriage.
# Season 02, Episode 03
## Julie's Dilemma
Julie, the cruise director, is expecting her parents on board. Turns out her father is
Mister Roper (Three's Company).
He and the Mrs let Julie know they're getting a divorce. Julie is pissed, but starts trying to hook them up with sexy singles on board. They don't like it and decide to get back together. Julie gets a compliment from her boss for hooking up two more people.
## Who's Who?
A TV censor winds up falling for a prude who passes out pamphlets about moral vices. They have a lot of stupid interactions where they try not to kiss, then they finally kiss and it's really traumatic for both of them. Then they discover they're roommates, and decide to get married. A whole lot of bedroom tomfoolery ensues, proving that even prudes like TV censors can find love, however unlikely.
## Rocky
A girl who wears baseball caps and sports jerseys befriends Danny Zuko's (Grease) nephew. She tries dressing up all girly for the boy, but he hates it. She decides to go back to presenting the way she's comfortable. Then they find out she's moving to his school district, and he asks her to a dance.
# Season 02, Episode 04
## The Man Who Loved Women
A dude dates three women at the same time and they find out, but decide he's a pretty okay dude.
## Oh, My Aching Brother
Some guy fakes a back injury but gives up the charade after a flaky woman with a torpedo bra shows interest in him.
## A Different Girl
A couple find out they both slept with someone else, but decide maybe that's okay.
The lady who played the wife put in a pretty stellar performance that the postproduction team
seems to have chosen to ignore.
When she tells her husband how excited she is that she's been offered a big promotion,
he tells her he'd prefer she stays at home.
> ... you'd be good at anything you tried. But the thing you're best at is being my wife.
> Boy do I brag about you to my buddies overseas.
> I told those guys you can cook up a Sunday dinner that would put the Ritz to shame,
> and that you've got a shape that makes Charlie's Angels look like Hogan's Heroes.
>
> ![a comically incredulous look from the wife](you-joking.jpg)
Of course, her incredulity that he'd respond this way
is brushed away immediately by editing.
# Season 02, Episode 05
## Julie's Aunt
The captain's uncle shows up and starts aggressively sexually harassing Julie (the cruise director). I'm talking like physical assault: grabbing her, throwing her to the ground, while the laugh track rolls on. She repeatedly seeks the help of her (men) co-workers, who are sympathetic but reluctant to intervene. Eventually she convinces Gopher to dress as her police inspector aunt to thwart his advances, which he does. Then the uncle starts harassing Gopher, but quickly repents when he realizes Gopher is a dude.
## Where Is It Written?
An author and his editor board the ship, with the editor's wife. The editor won't stop working, so the wife hangs out with the author instead. He keeps putting the moves on her, which she rejects. Eventually the editor notices, and they have a ridiculous fistfight which results in them all getting dumped into the pool. This convinces her that she actually still likes her wet blanket of a husband, and he agrees to stop working while on vacation, just as the vacation ends.
## The Big Deal
Some woman shows up with her dad and her dad's business partner. The dad's business partner lecherously eyes her and decides somehow that she's part of the business deal. She goes along with it because she wants her dad to succeed, but then she meets
Ponch (C.H.I.P.S.)
and realizes she still holds a candle for him from high school. Lecherous guy gets all jealous and I don't know what happens next because I stopped caring.
# Season 02, Episode 06
## The Witness
Mike Brady (The Brady Bunch)
was a witness to some heinous crime. He falls in love with some lady who also knows about the crime, and the crime weighs on him so much that their relationship is tortured and boring. Oh, snap! The murder victim was her brother! What are the odds?
## Mike and Ike
Isaac the Bartender recognizes a family (they're also black, of course). Turns out the three adults were in a doo-wop band together. The dad nows owns a bunch of car dealerships and can't stay off the phone long enough to hang out with his son. Isaac tries to convince the dad to spend time with his son. I actually like how Isaac was portrayed here, his character starts to show some depth.
## The Kissing Bandit
Miracle Max (The Princess Bride)
dresses up every night with a mask, cape, and fedora, and runs around the ship grabbing women and kissing them. Meanwhile there's a woman actually interested in him during the day, and he's skipping out on her to do his kissing rounds at night. This one actually made me laugh, a Love Boat first. Right until the crew decides to use Julie as "bait" to catch the kissing bandit. In hindsight, maybe sexual assault isn't all that funny.
The actress who played Julie wound up addicted to cocaine, and was one of the very first Hollywood people to openly admit they had a drug problem. They dropped her from the show, but she did make a full recovery right around the time The Love Boat ended its run.
# Season 02, Episode 07
## Ship of Ghouls (Story 1)
Some white lady is all self-conscious because she has a scar on her face.
## Ship of Ghouls (Story 2)
The owner of The House on Haunted Hill is an illusionist whose wife is upset that he can't leave his work behind.
## Ship of Ghouls (Story 3)
Some kid is worried that his dad might leave for a whole year again, which makes him a pathological liar.
Two stories intersect when the white lady is so upset about her scar that she decides to jump overboard.
The lying kid who cried wolf can't convince anybody he found her on the rail,
so he talks her down himself with a lie about how his mom committed suicide.
# Season 02, Episode 08
Julie's sporting a new hair style in this episode.
## Anoushka
Hot Lips Houlihan (M.A.S.H.)
boards the ship as a godless commie cruise director,
in some sort of east/west cruise staff exchange program.
She talks to Julie about the glass ceiling,
making this perhaps the first episode that passes the Bechdel test.
The doctor gets bored and wanders off.
Comrade Hot Lips asks Julie to do her hair and makeup,
then shows up to dinner looking sexy.
Suddenly the doctor wants to talk to her,
proving, I guess, that the doctor is a pig.
Comrade Hot Lips decides she's way more interested in the doctor
than her career, and she spends the rest of the episode pursuing him.
He proposes marriage at the end of the 3-day cruise,
but she has to go back to Russia.
## Accidental Cruise
Soupy Sales shows up drunk with his secretary.
Turns out they don't have tickets, they thought they were going to his apartment.
She confesses her love to him,
but he just wants her to take dictation.
I'm deeming this "Love Boat Plot #1":
man can't stay away from work.
Toward the end, she forcibly kisses him,
prompting the crew to use the word "assault" for the first time
this season.
## The Song is Ended
Susan/Sharon's older sister and her husband
take their first vacation without kids.
They run into the host of Family Feud,
and realize their marriage has failed.
But then the game show host sings a song,
and they decide to stick together.
The actors almost got a tear out of me.
Could just be Love Boat Fatigue though.
## A Time for Everything
Within 30 seconds it becomes clear the captain is a father to an orphaned love child.
The rest of the story proceeds to slowly reveal additional clues,
in case the viewer is an idiot.
The captain spends a lot of time staring off-screen as the camera zooms in.
Sometimes this triggers a flashback scene,
other times we just watch him staring.
Then the little girl leaves,
and the captain reads through a bunch of love letters he's kept from her mom.
Between this attempt at a tear-jerking ending,
and Hot Lips crying about having to go back to Russia,
this probably qualifies as "A Very Special Love Boat"
# Season 02, Episode 09
## Till Death Do Us Part
JJ (Good Times)
and
Vy (The Fresh Prince of Bel-Air)
board,
but it turns out she killed him by putting lighter fluid on his cole slaw,
and he's a ghost now.
JJ is pretty funny,
and Julie puts in some good supporting acting,
preteding JJ is invisible:
![Julie looking confused](who-you-talking-to.jpg)
JJ Tries to hook her up with
Barney Collier (Mission: Impossible)
and a bunch of other (black) guys.
But because she's having a conversation with a ghost,
she winds up saying all sorts of off-putting things to the (black) people
who keep coming up to her.
It's just that same joke,
repeated for 30 minutes,
and interspersed with JJ tossing out one-liners
that sidle right up next to being funny.
JJ gets all butthurt when she winds up actually liking Barney,
and tells her 2 years of mourning isn't enough.
But he has a change of heart
and pushes her down the stairs,
which causes Barney to propose.
## Chubs
Gopher's sister is on this cruise.
Seems she was fat when she was younger,
giving the crew an opportunity to make a bunch of fat jokes.
Ha ha! Fat jokes!
Anyway, she finally shows up,
and what do you know,
Gopher's sister is
Hot Mary Ingalls (Little House on the Prairie)!
Boy, is she hot!
And she's looking for some action!
She keeps trying to hook up with doc,
but he's put off because,
"you're 18 and you're Gopher's sister".
So the recurring character estableshed as
"mister casual hook-up" gives Hot Mary Ingalls a
lecture about holding out for true love.
## Maybe
Laurie Strode (Halloween)
and
her husband, "Sir Not Appearing In The Credits",
are annoyed when Laurie's parents show up.
Husband tries repeatedly to engage her,
but she's too busy being a jerk,
having decided that because her parents divorced,
she will too.
On seeing that her parents have made up,
Laurie decides not to divorce Husband after all.
## Locked Away
Mister Drummond (Diff'rent Strokes)
and
Marion Crane (Psycho),
Laurie's divorced parents,
get locked in a room together.
They *hate* each other.
Being locked in the same room forces them to finally work through their issues,
and they leave a happy couple again.
In earlier days,
I would have thought that
"doorknob came off, now you're locked in"
was pretty flimsy.
But recently I got to witness three teenagers
spend four days failing to open a vacation home's front door from the inside.
So maybe this isn't as ridiculous as I thought.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,98 @@
---
date: 2023-01-25
title: "The Love Boat s02e10"
tags:
- love-boat
- tv
---
I'm still watching The Love Boat.
I find it interesting that this is easier for me to stick with than Doctor Who.
# Season 02, Episode 10
It's thanksgiving on board the Pacific Princess!
And presumably off-board too.
At least, in the United States.
## Tony's Family
The ship engineer called in sick,
so they ask Tony to stay on another shift,
but he's mad because he was going to spend the holiday with his family.
So they're like, well, just bring them on board.
Womp womp, turns out he has six people in his family.
And Tony's mom brought a chicken.
Batten down the hatches, here come the immigrant jokes!
Actually, aside from the chicken thing,
this wasn't as bad as I expected.
Maybe I'm getting normalized to 1978 suburban America,
but I felt like most of the gags fell into two categories:
1. The chicken
2. Struggling to keep six people hidden from the captain
Anyway, they get discovered,
and the captain says they have to pay their fares,
so the crew offers to pool their money together to pay $1500 for everyone.
But then the young boy gives a dollar to a gambler from another story,
and God descends from the machinery.
The gambler returns with about $75 in coins from the slot machine,
and gives it to the family,
who uses it to pay off their $1500 debt in entirety.
## The Minister and the Stripper
A lady gets upset that her minister,
the captain from Airplane!,
falls for a stripper on the cruise,
and threatens to have him kicked out by the trustees or the board or something.
That means they're Presbyterian, I think?
I never could remember the intricacies of all the protestant governance structures.
Her husband threatens to leave her,
and she sees the errors of her ways,
deciding to use her influence in the governance body to defend the minister's decision.
Then the husband gambles a broke kid's last dollar,
turning it into about $75.
Costuming must have had fun with these two:
![big hair and intense makeup with a giant necklace](makeup.jpg)
![purple jacket, yellow puffy shirt, and giant bow tie](wonka.jpg)
This one struck me as actually still relevant today.
And the captain from Airplane! did a believable job as a minister.
I heard they cast well-known actors to "play it straight" in the Airplane! movies,
and boy howdy, this guy was a good casting decision.
## Her Own Two Feet
A newly-blind lady doesn't want to admit she's blind.
The ship's doctor convinces her husband that his doting and excuses
are making it difficult for her to face reality.
The husband abandons her in their room with a cane,
telling her he'll be in the lounge and he hopes she comes to meet him.
She steels her resolve and gets herself down the hall on her own.
I'll admit it: this one made me cry.
An episode of The Love Boat made me cry.
What the heck is wrong with me?
---
This show had a habit of hiring Hollywood stars from the 30s and 40s,
who weren't getting a lot of work due to their age.
They got some pretty impressive performances this way.
The "blind" lady was played by June Allyson:
a headliner, judging by posters from the films she starred in.
Her husband was played by Van Johnson, her frequent co-star.
(A previous release of this confused Van Johnson with Alan Young.)
Here's Van expressing... grief? asphyxiation?
I've decided to call it "the eyeroll of despair".
{{<video src="eyeroll-of-despair.mp4" text="A man has a distraught woman in his arms, in a comforting hold. His brow is furrowed, he is rolling his eyes, and his mouth is hanging open.">}}
I wonder how many takes they did of this?

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,91 @@
---
title: The Love Boat s2e11
date: 2023-01-30
tags:
- love-boat
- tv
---
Season 2, Episode 11
This was the worst one yet.
## Mona of the Movies
Mona Maxwell (1940's Awooga-Babe Rhonda Fleming)
is met by a bumbling fan (frequent Johnny Carson guest Orson Bean).
For some reason,
she agrees to have dinner with him.
Then she cancels because she already agreed to have dinner with the captain,
which makes him mad.
She dances with him after dinner,
and they hit it off,
but she can't go to his cabin,
which makes him mad.
I can't keep up with this plot,
but toward the end,
I heard some of the coolest jazz fusion I've ever heard on a TV show.
Sounds almost like Spyro Gyra.
## Heads Or Tails
Two dudes won't stop harassing Julie,
who puts up with it good-naturedly.
I'd really like to see how the writers handle the men staff getting harassed.
Maybe one episode I'll get to see it.
Anyway, she agrees to have dinner with one of them and go dancing with the other,
demonstrating to the television audience how to talk to women.
They keep up with the harassment,
and Julie tries to teach them how to stop.
One of them convinces Julie he's hurt,
and she walks him to his room,
where he upgrades to full physical assault,
grabbing her and throwing her to the bed.
Gopher tells her "it was a harmless little joke",
and tries to convince her to give them yet another chance.
There's a scene where Julie actually stops putting up with everybody's crap,
and I was really rooting for her.
But next scene she's back to being a pawn, sigh.
At the end of the episode,
she takes them both out to dinner on land.
What the heck?
## The Little People
Oh boy.
Based on what I've heard about how Tattoo was written in Fantasy Island,
I'm expecting a lot of cringe from this.
Noodles MacIntosh (UHF)
and Lumpy (Star Wars Holiday Special)
show up with their son.
Every time anyone (including himself) says "little" or "small",
Noodles comments on it. Eugh.
The son immediately meets
Rhoda "Serial Killer Jr" Penmark (The Bad Seed),
who he recognizes from the elevator.
They're immediately smitten with each other.
Aaand that's the comic shtick, folks.
Noodles pointing out every time anybody says "short" or "little" or "small".
Rhoda finally sees mom and dad,
and appears to have some pretty serious prejudices.
The son can't handle it,
and breaks it off with her.
Then she runs into mom and dad in the bar,
and they inadvertently help her understand why being a jerk was off-putting.
Then everybody realizes who everybody else is.
They decide to get married,
and she says it's okay if their children are small,
concluding the 16-minute story arc.

View File

@ -0,0 +1,79 @@
---
title: Trains
date: 2023-03-07
tags:
- climate
- trains
---
This year I am trying to spend less time on airplanes,
as part of a global movement motivated by carbon emissions concerns.
Amtrak, the US national passenger rail company
(the *only* national passenge rail company in the US),
is in a really sad state right now.
They keep getting their budget cut,
and freight companies have found several innovative ways
to save money at the expense of safety and passenger rail quality.
So rail exists in the US, technically,
but it is not generally considered a desirable way to go.
Maybe that will change as people seek non-airplane methods of travel.
The place where I work will let me take rail,
but they won't reimburse more than the equivalent flight.
This has never been a problem for me:
flying has always come in more expensive than train.
On this upcoming trip, I have to stay overnight in Chicago,
because there is only one train per day on each line.
Even though the train is $315 less than the cheapest flight,
I am not allowed to apply any of that toward lodging reimbursement.
If you assume people are going to take the option that
lines up closest to their personal economic interest,
that's 2 points against the train,
both of which carry an outsized societal burden.
Point 1: Time
-------------
My time is worth something to me,
and taking 4 additional days for a work trip kinda sucks.
A lot of people will stop considering rail travel based just on this point.
There's a way to fix this,
by upgrading tracks so that passenger trains can go much faster,
and by finally addressing how the mega-long freight trains can't yeild to the passenger trains,
because there aren't stretches of side track long enough for them to do so.
(People who would know assure me that the freight companies are well aware of what they're doing to passenger rail)
This fix requires two things: a country willing to spend lots of money on passenger rail,
and the political will to do something that will make freight companies really upset.
I'm not seeing that the country is excited about any kind of infrastructure spending,
much less passenger rail.
And I'm definitely not seeing a country that wants to hurt any kind of commerce.
But maybe if more scientists are inconvenienced by rail,
the winds can shift.
Point 2: Money
--------------
Even though it costs fewer dollars for me to take the train,
my company is not set up to handle this.
The result is that I have to pay out of pocket for the hotel stays,
and I will not get reimbursed.
If taking over 3 times longer wasn't enough to dissuade people,
telling them it will also cost them $400 that won't be reimbursed
is going to make rail travel look like a bad decision.
The fix for this is easier:
my company needs to figure out a way to consider that the train is saving them money,
and allow me to spend some of that savings on the required hotel.
I've started trying to see what I can do to make that happen.
It may take a few years, and depends on senior management caring,
but I think it has a higher chance of happening.
If enough places figure out how to stope penalizing people for taking the cheaper option,
maybe there will be sufficient public interest in point 1 for something to happen there.

View File

@ -0,0 +1,127 @@
---
title: The Year 2038 problem
date: 2023-03-09
published: false
---
In the year 2038,
old Unix code is going to run out of bits to store time.
This is going to be a really big problem,
and I'm hoping I can surf it into retirement.
The Y2K Bug
===========
Back in the 1990s there was this increasing panic about the
"Y2K problem".
These days it seems to be remembered as sort of a joke,
like "ha ha remember how freaked out they tried to make us?
And it wound up being a whole lot of nothing."
A bunch of people,
including me,
worked their assses off to make sure nothing bad happened.
The problem was that a lot of systems written in the 1960s to the 1980s
stored the year as a 2-digit number.
You got the full year by adding 1900 to that stored number.
This made total sense,
because most people were used to writing the year as,
like, "63" or "89".
I was taught in school to use "mm/dd/yy" format on anything needing a date.
So when it became increasingly clear that "add 1900" wasn't going to work
when the year became "00",
a whole lot of people had to go dig through a whole lot of old programs,
and patch them in various ways to deal with 2000 and beyond.
Here are a few of the main techniques I saw,
in order from "patching it up with chewing gum" to
"fix that will work forever":
* If the number is 60-99, add 1900. Otherwise add 2000.
This works only if you're storing dates after 1960,
and is going to stop working in 2060.
I guess at this point everyone involved in the 1999 fix
will probably be dead, so it'll be someone else's problem.
* Alter things to store years with more than two numbers.
So the year 2000 is stored as 100 (1900 + 100 = 2000).
It's weird when you look at the raw data,
but it might mean you can keep using the rest of the program,
which means you're able to get the fix in quickly.
* Store time as a `time_t`, which counts the number of seconds
since January 1, 1970 in Coordinated Universal Time (UTC).
I'll talk about the problem with this below.
* Store the year as a 4-digit number,
which will keep working for another 8000 years,
and also allows you to store dates going back to the beginning
of most governments on Earth.
When January 1, 2000 rolled around,
there were still a couple of problems.
I remember one payroll company was dating checks wrong,
I think an insurance company had some sort of issue they patched pretty quickly,
but for the most part,
everything kept running along smoothly,
and all the people like me who had stayed up all night in the data center
waiting for some big emergency
breathed a big sigh of relief and went to bed.
The Year 2038 Problem
=====================
That third example of a fix uses the Unix `time_t`,
which is a 32-bit signed integer.
Bits?
-----
Bits are talked about a lot with computers,
but hardly ever explained.
Here's a really brief introduction
based on my years of experience teaching binary to high school students.
Let's start by talking about decimal, though.
| # of digits | how many muffins you can count | number of different values |
| ---- | ---- | ---- |
| 1 | 0 - 9 | 10 |
| 2 | 0 - 99 | 100 |
| 3 | 0 - 999 | 1000 |
| 4 | 0 - 9999 | 10000 |
Get it?
Now let's talk about binary:
| # of bits | how many muffins you can count | number of different values |
| ---- | ---- |
| 1 | 0 - 1 | 2 |
| 2 | 0 - 3 | 4 |
| 3 | 0 - 7 | 8 |
| 4 | 0 - 15 | 16 |
| 5 | 0 - 31 | 32 |
| 6 | 0 - 63 | 64 |
| 7 | 0 - 127 | 128 |
| 8 | 0 - 255 | 256 |
See the pattern?
Every time you add a bit,
you get twice as many values.
Let's extend that table:
| # of bits | how many muffins you can count | number of different values |
| ---- | ---- |
| 1 | 0 - 1 | 2 |
| 2 | 0 - 3 | 4 |
| 3 | 0 - 7 | 8 |
| 4 | 0 - 15 | 16 |
| 5 | 0 - 31 | 32 |
| 6 | 0 - 63 | 64 |
| 7 | 0 - 127 | 128 |
| 8 | 0 - 255 | 256 |
| ⋮ | ⋮ | ⋮ | ⋮ |
| 30 | 0 - 1073741823 | 1073741824 |
| 31 | 0 - 2147483647 | 2147483648 |
| 32 | 0 - 4294967297 | 4294967296 |

View File

@ -0,0 +1,10 @@
---
title: Universal Date Format
date: 2023-03-14
---
YYYY🌍MM🌙DD
If we use this, we won't have to re-do date formats again when we colonize another planet.
Posting here to bolster any future claim that I thought of this first.

View File

@ -0,0 +1,77 @@
---
date: 2023-04-14
title: Kaktovik numerals
scripts:
- kaktovik.mjs
draft: true
---
I just saw a Scientific American article about the recent inclusion in Unicode of the
[Kaktovik Numerals](https://en.wikipedia.org/wiki/Kaktovik_numerals),
a base-20 counting system invented in 1990 by schoolchildren in northern Alaska.
Let's do some counting!
<div class="flex wrap counter" data-min="0" data-max="19">
<div>
<h3 class="justify-center">Apple-inator</h3>
<button class="add" data-amount="-1">- 🍏</button>
<button class="add" data-amount="+1">+ 🍏</button>
</div>
<div>
<h3 class="justify-center">Apples</h3>
<div class="apples"></div>
</div>
</div>
Seems pretty easy, right?
I had it group apples in rows, so it's easier to visually see how many apples you have.
Let's use 🍊 instead of 🍏🍏🍏🍏🍏,
to make it even easier to see how many rows we have.
<div class="flex wrap counter" data-min="0" data-max="19">
<div>
<h3 class="justify-center">Apple-inator</h3>
<button class="add" data-amount="-1">- 🍏</button>
<button class="add" data-amount="+1">+ 🍏</button>
</div>
<div>
<h3 class="justify-center">Fruit</h3>
<div class="fruit"></div>
</div>
<div>
<h3 class="justify-center">Apples</h3>
<div class="apples"></div>
</div>
</div>
---
<div class="flex wrap counter" data-min="0" data-max="19">
<div>
<h3 class="justify-center">Apple-inator</h3>
<button class="add" data-amount="-1">- 🍏</button>
<button class="add" data-amount="+1">+ 🍏</button>
</div>
<div>
<h3 class="justify-center">Kaktovik</h3>
<div class="kaktovik justify-left"></div>
</div>
<div>
<h3 class="justify-center">Fruit</h3>
<div class="fruit"></div>
</div>
<div>
<h3 class="justify-center">Apples</h3>
<div class="apples"></div>
</div>
<div>
<h3 class="justify-center">Arabic</h3>
<div class="arabic justify-right"></div>
</div>
</div>
Can you see how
the number of left-and-right lines is the number of complete rows of apples,
and the number of up-and-down lines is the number of apples left?

View File

@ -0,0 +1,113 @@
const FruitNumerals = [
"", "🍏", "🍏🍏", "🍏🍏🍏", "🍏🍏🍏🍏",
"🍊", "🍊🍏", "🍊🍏🍏", "🍊🍏🍏🍏", "🍊🍏🍏🍏🍏",
"🍊🍊", "🍊🍊🍏", "🍊🍊🍏🍏", "🍊🍊🍏🍏🍏", "🍊🍊🍏🍏🍏🍏",
"🍊🍊🍊", "🍊🍊🍊🍏", "🍊🍊🍊🍏🍏", "🍊🍊🍊🍏🍏🍏", "🍊🍊🍊🍏🍏🍏🍏",
]
const KaktovikNumerals = [
"𝋀", "𝋁", "𝋂", "𝋃", "𝋄",
"𝋅", "𝋆", "𝋇", "𝋈", "𝋉",
"𝋊", "𝋋", "𝋌", "𝋍", "𝋎",
"𝋏", "𝋐", "𝋑", "𝋒", "𝋓",
]
function ToNumerals(numerals, n) {
let base = numerals.length
let its = []
if (n < base) {
return [numerals[n]]
}
while (n > 0) {
its.unshift(numerals[n % base])
n = Math.floor(n / base)
}
return its
}
function ToFruit(n) {
let its = ToNumerals(FruitNumerals, n)
let doc = new DocumentFragment()
for (let it of its) {
let row = doc.appendChild(document.createElement("div"))
row.classList.add("row")
row.textContent = it
}
return doc
}
function ToKaktovik(n) {
let its = ToNumerals(KaktovikNumerals, n)
return its.join("")
}
function ToApples(n) {
let doc = new DocumentFragment()
while (n > 0) {
let apples = Math.min(n, 5)
let row = doc.appendChild(document.createElement("div"))
row.classList.add("row")
row.textContent = "🍏".repeat(apples)
n -= apples
}
return doc
}
class Counter {
/**
* Initialize a counter element.
*
* This makes buttons active,
* and does an initial render.
*
* @param {HTMLElement} element
*/
constructor(element, n=1) {
this.element = element
this.n = n
this.min = Number(this.element.dataset.min) || 0
this.max = Number(this.element.dataset.max) || Infinity
for (let e of this.element.querySelectorAll("button.add")) {
let amount = Number(e.dataset.amount) || 1
e.addEventListener("click", e => this.add(e, amount))
}
this.render()
}
add(event, amount) {
let n = this.n + amount
n = Math.min(n, this.max)
n = Math.max(n, this.min)
if (n != this.n) {
this.n = n
this.render()
}
}
render() {
for (let e of this.element.querySelectorAll(".kaktovik")) {
e.textContent = ToKaktovik(this.n)
}
for (let e of this.element.querySelectorAll(".fruit")) {
while (e.firstChild) e.firstChild.remove()
e.appendChild(ToFruit(this.n))
}
for (let e of this.element.querySelectorAll(".apples")) {
while (e.firstChild) e.firstChild.remove()
e.appendChild(ToApples(this.n))
}
for (let e of this.element.querySelectorAll(".arabic")) {
e.textContent = this.n
}
}
}
function init() {
for (let e of document.querySelectorAll(".counter")) {
new Counter(e)
}
}
document.addEventListener("DOMContentLoaded", init)

View File

@ -0,0 +1,33 @@
---
title: New Job
date: 2023-06-20
---
I'm now working at Pacific Northwest National Laboratory.
This is a Department of Energy Office of Science laboratory.
LANL, my previous employer,
was a National Nuclear Security Administration laboratory.
PNNL is going to have less restrictive policies,
and more of a focus on science that can be published to the world,
at the cost of not being as well-funded.
It's also a fully-remote job.
At PNNL, this means I only go in to work 3-4 times a year.
Most of my time will be sitting in my bedroom,
but they're fine with me working in a coffee shop,
or on a train,
or in a VRBO with my wife while she's doing things for her advanced degree.
This was the roughest job switch I've ever done.
It was stressful enough that I think it is unlikely I will ever switch jobs again.
I'll be doing pretty much the same thing I was doing at LANL.
I'm still managing Cyber Fire,
and I'll be helping out with some other training things at PNNL.
This is also my first attempt to use Windows for work.
I haven't used Windows since about 1993.
It seems to have improved: I only have to reboot once a day now.
So that's nice.

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -0,0 +1,142 @@
---
title: Business Travel on Amtrak
date: 2023-09-25
tags:
- climate
- trains
---
My new year's resolution for 2023 was to avoid airplanes whenever possible. Let's see how I'm doing!
| Dates | Transport | Trip Description | Comment |
| --- | --- | --- | --- |
| Sep 29 - Oct 8 | Rail | Business trip to Washington | |
| Oct 30 - Nov 18 | Car | Business trip to Colorado | |
| Oct 20 - 30 | Rail | Business trip to Washington | 1st rail trip with a bicycle |
| Jul 30 - Aug 18 | Air| Three back-to-back trips | Only possible with air travel |
| Jul 22 - 23 | Car | Vacation to Colorado | |
| Jun 5 - 16 | Car | Vacation with my father to Montana | |
| Jun 1 - 4 | Car | Dance trip to Arizona | |
| May 21 - 23 | Car | Freshman college orientation | |
| May 5 - 14 | Rail | Business trip to Minneapolis | |
| Apr 21 - 23 | Air | Dance performance in Chicago | Everyone else on this trip was flying |
| Mar 26 - 28 | Air | Job interview in Washington | I had almost no input into how this was booked |
| Feb 9 - 13 | Car | Dance trip to San Diego | I don't remember if I went on this trip or not! |
| Dec 19 - Jan 3 | Air | Vacation to Iceland | This trip convinced me to try rail |
Not bad for year 1. It took a few months for me to get into it, and there are still things I cannot do
any other way, but overall I'm happy with how this is going.
People seem to be a combination of amused and curious about taking Amtrak to get somewhere.
Most folks think of Amtrak as a quirky but impractical curiosity, or maybe like an amusement park ride.
A lot of people are under the impression that rail is more expensive than air (rail is almost always cheaper),
or that one must get "a sleeper car" (coach on rail is like first class on air).
I will say that this is not practical. There is one train per day on the routes I take, and they are frequently
delayed, which can cause a full day layover if I miss a connection. Amtrak's web site won't let you reserve a
multi-leg trip that involves an overnight stay. There's no WiFi, and cell coverage is spotty. If you are the sort
of person who's in a hurry, rail travel will drive you up the wall. Rude passengers are rude for *two days*,
not just a couple hours until you land. Getting your company to let you go by rail might range from frustrating to
impossible.
However, I am now in a life situation where I can do this: I don't have children at home, I have a fully
remote job that lets me work wherever, and I have an employer who can wrap their head around rail travel.
Maybe if I do this enough, I can convince a couple other people to do this.
And eventually we'll have more ridership, and maybe Amtrak will get better.
I wish I were capable of Great Feats in order to help deal with climate change.
It allows me to continue to be employed, while pretty drastically reducing the carbon emissions I am
personally responsible for.
Maybe undergoing this pain in the rear,
and documenting it, will pave the way for others.
So it's going to have to be my small role to play here.
Booking / Expensing business trips
-------------
LANL let me take whatever means of travel I wanted, I just had to demonstrate it was equal to, or below, the cost
of going by air. It always is.
PNNL makes it even easier: I just tell them I want to get to city X by date D by rail, and a travel agent figures
it all out for me, including overnight stays. I'm still figuring out the itinerary beforehand, because I'm that way,
but so far their bookings have been fine.
LANL would reimburse me for the trip, but I had to add "personal days" onto the trip for the extra time spent
en route. I only took two rail trips at LANL, and both ate weekends. There is a process for obtaining approval
for working someplace other than my house that I never had to attempt. LANL would not reimburse an overnight
hotel stay on a personal day, even when I demonstrated that the train fare + hotel was still cheaper than air.
I was preparing for a years-long fight to try to move the needle on this one, but then I switched jobs.
PNNL has yet to reimburse me for a rail trip, but I think it's going to be similar to LANL. They may allow me to
consider the airfare against the rail+hotel--I'll try--but it seems more likely that they will also make me pay
the hotel myself. However, because I have a remote work position, I don't need to request permission to work
on a train: I can just tell my manager I'm doing it.
What to take
------------
{{< figure src="backpack.jpg" alt="Contents of my backpack: only a few articles of clothing, some wires, food, and the things I list below" >}}
Here are some essentials for a train trip that air travelers might not have considered:
### Extension cord
A long extension cord provides power to the aisle seat without needing to drape USB cables over the window
seat's lap.
### Washcloth and Towel
A travel washcloth and towel can help you clean up in the restroom on long trips.
### Blanket
I like being under a blanket when I sleep.
I bring a fleece blanket that rolls up to about the size of my towel.
I wonder if I could find a towel that doubles as a blanket? Hmm.
### Eye mask
I made an eye mask out of some left-over Batman print fabric. It's cool.
It also blocks light well enough that I can stay asleep.
### Earplugs
If you hadn't guessed yet, I'm a light sleeper.
The train is pretty quiet, which means little noises stand out.
Earplugs are essential for me.
I've slept with wired earbuds, which is okay, especially when they're making a little (quiet) noise.
I just got some fancypants bluetooth sleep buds that can
play music/audiobooks and generate white noise to mask outside noise.
These bluetooth things are even smaller than the wired earbuds,
which so far has been nice for sleeping on my side.
### Food
Unless you're made of money,
you're going to get tired of the dining car.
I bring some fruit and other perishables that can stay edible for a few days,
thinks like hummus or guacamole.
Crackers and nuts are great for a meal or for snackin'.
I don't mind eating Tasty Bites cold, so those are nice meals.
### Entertainment
You are going to get bored. Bring something to do!
I bring my work laptop, and crank on code or reports.
Remember to download everything before you leave:
there's no WiFi,
and tethering to my phone has been spotty at best.
You absolutely, positively, must download all your TV shows and movies before you go to the train station.
Cell service is spotty at best, so you can forget about downloading
new episodes en route.
I use my phone for watching movies and whatnot. I printed a
[flat phone stand](https://www.printables.com/model/175598-phone-stand) to hold it up on the tray.
The seat in front of you is about 5 feet away, so attaching your phone to the seat back doesn't really work.
I'll probably bring my Nintendo Switch again, but I don't think I used it much on the last trip.
I'm not much of a gamer.
I do tend to read a lot of books, though.
For me, it's important to be already in a book I'm interested in.
I have trouble starting a new book on the train.

View File

@ -0,0 +1,25 @@
---
title: High School Reunion
date: 2023-09-26
---
> ... many of us have simply moved on from high school,
> and feel no need to revisit the awkward hallways of our youth.
- [Laura Tartocci, Ph.D.](https://www.psychologytoday.com/us/blog/you-can-t-sit-us/202203/should-you-go-your-high-school-reunion)
---
A few people from high school keep reaching out to me,
year after year,
asking if I'm going to the reunion this year.
I am not.
I like the person I am now!
I acknowledge that high school me and college me were necessary steps toward adult me,
but stepping into my high school shoes again is not an appealing proposition.
I understand that this calculation is different for everyone.
But
I personally have little to gain from going to the reunion.

View File

@ -0,0 +1,35 @@
---
title: Train Bag
date: 2023-10-10
tags:
- trains
---
I like to carry the things I need easy access to in a train bag.
I hook this over the leg rest lever,
and then I can just kinda chill and not need to get to my backpack at all.
Here's what's in my bag:
* Extension cord
* USB Power Supply & Battery
* Two USB C charging cables
* One USB Micro charging cable
* E-book reader
* Earbuds
* Sleepin' earbuds
* Thing I cram in my nose to keep me from snoring as much
* Practice bagpipe and wired earbuds
* Nintendo Switch if I brought that
* Resistance band for getting the ol' muscles moving
* Maybe a pair of reading glasses
* Whatever other non-food junk I feel like I might want
I typically leave that bag there for the entire trip.
If I head into the observation car, I can bring the bag with me,
and it's got my book and glasses and charger.
When it's time to deboard,
I can grab the train bag,
cram it into the backpack,
and I'm all set.

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,168 @@
---
title: How I Get from Los Alamos to Richland
date: 2023-10-21
tags:
- trains
---
Before I begin my trip,
I pack up enough food for four days.
It's only a 3-day trip if the trains are all on time,
but sometimes there are big delays,
so I prepare for that.
So far this means 6 tasty bite Indian food pouches,
6 apples,
a box of crackers,
a chocolate bar,
two oat bars that I never eat,
six hard boiled eggs,
four single-serve guacamole cups,
and six single-serve hummus cups.
The eggs, guacamole, and hummus go in a little insulated lunch pail,
along with an uninsulated canteen full of ice.
{{<figure src="PXL_20231020_170034557.MP.jpg" alt="An insulated lunchbox with 6 eggs, an uninsulated canteen full of ice, and some single-serve packets of hummus and guacamole">}}
Los Alamos to Lamy
-----------------
There are a few ways I could get from home in Los Alamos to the Lamy train station.
There's a nearly-direct shuttle for $80.
I could take a series of two busses for maybe $60 and around 2 hours:
I haven't looked into this one too closely because 2 hours is a lot.
Or I can drive or have someone else drive me for 40 minutes.
So for I always drive.
If I take the Nissan LEAF, I have to charge it in Santa Fe before I get to Lamy,
so that I have enough of a charge to make it back to Santa Fe when I return.
Lamy to Los Angeles
---------------------
Amtrak route 4, the Southwest Chief, goes from Chicago to Los Angeles, through Lamy.
It arrives around 13:40 when it's on time, but it's frequently 30-40 minutes late.
There's a little brewpub in the train station that opens at 14:00,
so I'll typically buy a beer and chat with the owner if the train is late.
{{<figure src="PXL_20231020_200515621.MP.jpg" alt="A glass of beer on a wooden bartop">}}
About an hour after I board, the train stops in Albuquerque for a 40-minute service inspection.
I could use that time to walk to a nearby grocery store and pick up food,
but so for I've always brought my own food in a little cloth bag.
I like bringing Tasty-Bites: Indian food in plastic pouches.
They're okay cold, and they don't need to be refrigerated.
The sun sets around the time we get to Gallup.
I'll eat my dinner of a tasty-bite, maybe some crackers,
and an apple.
The bathrooms are downstairs in the car,
where I can brush my teeth and floss.
The seats in coach are much larger than first class seats on an airplane,
and recline about 45°.
{{<figure src="PXL_20231021_151322061.MP.jpg" alt="A Superliner coach seat, fully reclined, which is about 45° down" >}}
{{<figure src="PXL_20231021_151530454.MP.jpg" alt="A man sitting in a coach seat, with about 4 feet of space from his knees to the back of the chair in front of him">}}
I put a pillow under my shoulders and neck,
cover up with a fleece blanket,
put in some tiny noise-generating earbuds made just for sleep,
and try to get some sleep.
I wake up typically when we're in California,
having slept through all of Arizona.
I'll put some instant coffee in my steel mug and take it to the lounge car,
where they'll give me free hot water and ice.
Then I take my coffee back to my seat,
eat two hard-boiled eggs from my cold lunchbox,
and replace the melted ice in my uninsulated lunchbox canteen.
Downstairs to the bathroom to brush my teeth, wash up a bit, and change shirts.
I arrive in Los Angeles around 08:00.
The first thing I do is get my boarding pass for the next leg:
I like to do this as soon as possible so I can (hopefully) get a window seat.
I sleep better when I can rest my legs against the side of the car,
and I don't have to get up all the time to let somebody else out if I'm on the outside of the row.
Then I'll buy lunch for later.
If the next train is late, I get to eat it in a nice little outdoor courtyard at Los Angeles Union Station.
{{<figure src="PXL_20230930_161409234.MP.jpg" alt="The inside of Los Angeles Union station, with vaulted roof, chandeliers, and some art deco paint on the ceiling">}}
Los Angeles to Portland
------------------------
The Coast Starlight train leaves at 09:50, but is sometimes late.
Starting at Santa Barbara,
on the fireman's side of the train,
you are right next to the ocean,
which can be pretty.
{{<figure src="PXL_20231021_214043091.MP.jpg" alt="The view of the Pacific Ocean from the Coast Starlight, north of Santa Barbara">}}
I don't even know where I am when I have supper,
I think it's before the bay area.
When I wake up, I'm in Northern California, right near Mount Shasta.
I think.
{{<figure src="PXL_20231022_141420597.MP.jpg" alt="Mount Shasta just after sunrise.">}}
Around lunchtime,
the train goes up into some mountains,
and the Engineer's side is the one you want to be on to look out over the valleys.
{{<figure src="PXL_20231001_171312049.MP.jpg" alt="A view, from above, of fog settling into the valley of the Oregon forest">}}
On all these legs,
I try to put in 8 hours of work on weekdays,
and then there's a lot of movies, reading,
and just looking out the window.
It's a very different experience than travel by airplane:
you have no choice but to chill out and slow down.
I arrive in Portland at 15:40.
If I'm lucky, and the train arrives on time,
I can run out and buy something for supper.
Otherwise, it's another pouch.
Portland has a lot better food options near the train station than Los Angeles.
{{<figure src="PXL_20231002_003840252.MP.jpg" alt="Portland Union Station from outside: a red brick single-story building with vaulted roof and a tall clock tower">}}
Leg 4: Portland to Pasco
-------------------
The Empire Builder leaves Portland at 16:45.
I don't have many interesting things to say about this leg,
because it's usually dark.
One time I was delayed 12 hours,
and got to watch the sun rise over the Columbia river.
{{<figure src="PXL_20231002_142539895.MP_exported_917.jpg" alt="Sunrise over the Columbia river: a desert with a river running through it">}}
I arrive in Pasco at 21:06,
and call a taxi to take me to my hotel.
Getting Back Home
===============
While I'm working in Washington,
I'll stop by a grocery store to buy food for the trip back home.
It's similar going home,
except that the train from Los Angeles to Lamy leaves
before the train from Portland to Los Angeles arrives.
I have to spend almost 23 hours in Los Angeles.
So far that's been at a hotel.
If I find out that work won't reimburse that hotel night,
I'll start looking into cheaper options than a downtown LA hotel.
The reason work might not reimburse it is because I'm taking "personal travel days"
to do this trip on the train. They will only pay for what it would cost by plane.
And while the train + 1 extra hotel night is still less than the plane fare,
they might not be willing to consider the overall trip cost:
rather, they'll consider the cost of transport (train vs. plane),
and will refuse to pay the hotel visit for extra days.
We'll see!

View File

@ -0,0 +1,23 @@
---
title: Big Builder
date: 2023-10-27
tags:
- computers
---
I finally set up CI/CD with Forgejo/Gitea.
But I did it my way.
My way means:
* I can't run "gitea actions".
These are actually node.js programs that get checked out on demand
from somewhere (github probably).
I don't like that model.
* I can't spawn docker containers in my swarm from CI/CD.
* I have to prebuild an environment with any build tools I want.
That's the way I like it.
If you think that sounds nice,
go check out
[Big Builder](https://git.woozle.org/neale/big-builder).

View File

@ -0,0 +1,98 @@
---
title: Your First Train Trip
date: 2023-10-28
tags:
- trains
---
To my surprise,
people at work are considering taking the train places.
One of them might eventually decide to try it.
Here's my advice to you!
Start Small
-----------
You don't need to immediately try a 3-leg trip.
The first trip I took by train was Lamy to Kansas City:
you get on the train around 13:00, have dinner,
go to sleep,
then at about 07:00 you've arrived.
I recommend starting with a short trip like this,
so you can acclimate to everything.
Sleeping on a train, for instance,
is probably not something most people can do right away.
If you're in Richland, you might try a trip to Portland for the weekend.
That starts at about 06:45,
and arrives around lunchtime.
Plan For Delays
-------------
Delays happen with any type of travel.
On trains, delays tend to be hours long.
On my multi-leg trips, they're frequently 24 hours.
Mentally prepare yourself for delays.
Pack "buffer days" into your trip:
whole days where you don't absolutely need to be anywhere.
Put these on the tail end of each travel day:
when you arrive at the destination,
and when you get home.
That way, if you have a big delay,
you're just using your buffer.
If you're delayed overnight,
Amtrak will put you up in a hotel.
But you're not required to rush to the hotel immediately and watch TV!
You can get there whenever you like,
and do some touring.
This is an awesome perk of train travel!
Pack Food
--------
You can buy meals on the train,
but it's not cheap.
It's meant to be an upscale dining experience,
and the prices reflect it:
$25 breakfast, $35 lunch, $45 dinner in October 2023.
And that's before you pay the tip.
There is a café car,
but the food there is mostly things that can go in a microwave.
It does have beer and wine, which is nice:
you're not allowed to bring your own on the train.
But you can bring your own food.
Plan it like a picnic for your first short trip!
Pack Light
---------
Once you've accepted that delays are going to happen,
new opportunities open up for unplanned exploration.
I particularly relish getting stuck in Portland or Chicago:
both have big ol' downtown areas full of interesting things.
If you have a big suitcase to drag around,
exploration before you get to the hotel is not a viable option.
And you may find the hotel isn't anywhere interesting.
I love my REI Ruckpack 28 for travel.
It's got enough room for 3 changes of clothes (I do laundry every night),
my laptop, and all my other stuff.
I've got another blog entry that goes in more depth on
[what to take](/blog/2023/09-25-business-travel-on-amtrak/).
Chill Out
-------
Trains in the US are slow.
If you can accept this,
you'll have a nice time!

View File

@ -0,0 +1,19 @@
---
title: Self-Hosted Email
date: 2023-11-29
tags:
- computers
---
I am having trouble delivering mail to work from woozle.org,
despite going through Gmail.
I suspect at some point I'm going to have to tell everybody to give up on their
@woozle.org email addresses,
because email is not something you can get without throwing yourself at the mercy of a gigantic tech company.
This blog post is really an excuse to bookmark somebody else's blog post,
about the same problem,
because I keep needing to refer to it:
https://cfenollosa.com/blog/after-self-hosting-my-email-for-twenty-three-years-i-have-thrown-in-the-towel-the-oligopoly-has-won.html

View File

@ -0,0 +1,113 @@
---
title: Windows Customizations
date: 2023-12-15
tags:
- computers
- windows
---
Since I started at PNNL in summer 2023,
I've been using Windows as my primary OS.
It's going okay.
The previous time I tried to use Windows was 1993,
and it's gotten a lot better since then.
I'd say it's about as stable as Linux now,
with about the same number of annoying quirks.
I figured it might be helpful for me to keep a running list of things I've changed,
so when I get a new OS install,
I can sort of quickly get back up to speed.
WSL2
----
Step 1 is to set up WSL2.
I prefer WSL2 to WSL1 because filesystem access is so much faster.
It basically runs the same way as Linux on ChromeOS:
as a virtual machine with a translation daemon.
I had to install
[wsl-vpnkit](https://github.com/sakai135/wsl-vpnkit)
in order to work with the Cisco AnyConnect VPN client.
I think WSL2 uses something like Google's sommellier,
to translate Linux stuff like filesystems, X11, and Wayland,
into Windows.
It crashes a lot,
which can make Linux slow to a crawl,
or become unresponsive.
You have to kill the translation layer in an admin powershell
with
taskkill /f /im wslservice.exe
Then you have to relaunch WSL2.
Terminal
--------
Windows comes with two terminals:
Windows Console,
and Terminal.
Terminal is better.
It's actually better than a lot of Linux terminals:
it even supports
[OSC 52](https://github.com/theimpostor/osc),
which few Linux terminals currently support
(OSC 52 is the reason I was using foot).
The default bell sounds for a long time, and is too noisy for me.
You can change the bell by editing `BellSound` in the terminal's
[settings.json](terminal/settings.json).
I also set the terminal to automatically close on exit.
Debian had set up a `.bash_logout` that ran something which failed,
meaning bash always exited with an error code,
and the terminal wouldn't close right away.
Removing `.bash_logout` fixed this.
Git / Bash
---
You're going to want to install git.
In addition to providing git,
it also installs bash and a few other ported tools.
This makes `ls` act normally.
I think it also installs ssh,
but I'm not positive.
Visual Studio Code
------
These days I'm using Visual Studio Code.
It effectively blurs the difference between Linux and Windows,
at least while you're editing.
You tell VS Code to use bash by default,
with "Select Default Profile" in the little + launcher thing at the console.
Vim
----
I also use vim,
though.
I had to edit the system path to include the path to the vim binaries.
I still don't understand how to tell powershell to run an executable by path,
but whatever.
Now I can just `vim file` and it works.
Go
---
The Windows build of Go has been compiling my code with no changes whatsoever.
Pretty cool.

View File

@ -0,0 +1,321 @@
{
"$help": "https://aka.ms/terminal-documentation",
"$schema": "https://aka.ms/terminal-profiles-schema",
"actions":
[
{
"command":
{
"action": "copy",
"singleLine": false
}
},
{
"command": "paste"
},
{
"command":
{
"action": "splitPane",
"split": "auto",
"splitMode": "duplicate"
},
"keys": "alt+shift+d"
},
{
"command": "find",
"keys": "ctrl+shift+f"
},
{
"command": "unbound",
"keys": "ctrl+v"
},
{
"command": "unbound",
"keys": "ctrl+c"
}
],
"alwaysShowTabs": false,
"copyFormatting": "none",
"copyOnSelect": true,
"defaultProfile": "{58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530}",
"newTabMenu":
[
{
"type": "remainingProfiles"
}
],
"profiles":
{
"defaults":
{
"adjustIndistinguishableColors": "indexed",
"antialiasingMode": "cleartype",
"bellSound":
[
"C:\\Windows\\Media\\Windows Ding.wav"
],
"bellStyle":
[
"audible",
"window"
],
"colorScheme": "Tango Light",
"cursorShape": "filledBox",
"experimental.retroTerminalEffect": false,
"font":
{
"face": "Consolas"
},
"useAcrylic": true,
"useAtlasEngine": false
},
"list":
[
{
"commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"hidden": false,
"name": "Windows PowerShell"
},
{
"commandline": "%SystemRoot%\\System32\\cmd.exe",
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"hidden": false,
"name": "Command Prompt"
},
{
"guid": "{58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530}",
"hidden": false,
"name": "Debian",
"source": "Windows.Terminal.Wsl"
},
{
"guid": "{b453ae62-4e3d-5e58-b989-0a998ec441b8}",
"hidden": false,
"name": "Azure Cloud Shell",
"source": "Windows.Terminal.Azure"
},
{
"guid": "{e636fd95-8db6-56fc-969f-5647553fa92b}",
"hidden": false,
"name": "wsl-vpnkit",
"source": "Windows.Terminal.Wsl"
}
]
},
"schemes":
[
{
"background": "#0C0C0C",
"black": "#0C0C0C",
"blue": "#0037DA",
"brightBlack": "#767676",
"brightBlue": "#3B78FF",
"brightCyan": "#61D6D6",
"brightGreen": "#16C60C",
"brightPurple": "#B4009E",
"brightRed": "#E74856",
"brightWhite": "#F2F2F2",
"brightYellow": "#F9F1A5",
"cursorColor": "#FFFFFF",
"cyan": "#3A96DD",
"foreground": "#CCCCCC",
"green": "#13A10E",
"name": "Campbell",
"purple": "#881798",
"red": "#C50F1F",
"selectionBackground": "#FFFFFF",
"white": "#CCCCCC",
"yellow": "#C19C00"
},
{
"background": "#012456",
"black": "#0C0C0C",
"blue": "#0037DA",
"brightBlack": "#767676",
"brightBlue": "#3B78FF",
"brightCyan": "#61D6D6",
"brightGreen": "#16C60C",
"brightPurple": "#B4009E",
"brightRed": "#E74856",
"brightWhite": "#F2F2F2",
"brightYellow": "#F9F1A5",
"cursorColor": "#FFFFFF",
"cyan": "#3A96DD",
"foreground": "#CCCCCC",
"green": "#13A10E",
"name": "Campbell Powershell",
"purple": "#881798",
"red": "#C50F1F",
"selectionBackground": "#FFFFFF",
"white": "#CCCCCC",
"yellow": "#C19C00"
},
{
"background": "#282C34",
"black": "#282C34",
"blue": "#61AFEF",
"brightBlack": "#5A6374",
"brightBlue": "#61AFEF",
"brightCyan": "#56B6C2",
"brightGreen": "#98C379",
"brightPurple": "#C678DD",
"brightRed": "#E06C75",
"brightWhite": "#DCDFE4",
"brightYellow": "#E5C07B",
"cursorColor": "#FFFFFF",
"cyan": "#56B6C2",
"foreground": "#DCDFE4",
"green": "#98C379",
"name": "One Half Dark",
"purple": "#C678DD",
"red": "#E06C75",
"selectionBackground": "#FFFFFF",
"white": "#DCDFE4",
"yellow": "#E5C07B"
},
{
"background": "#FAFAFA",
"black": "#383A42",
"blue": "#0184BC",
"brightBlack": "#4F525D",
"brightBlue": "#61AFEF",
"brightCyan": "#56B5C1",
"brightGreen": "#98C379",
"brightPurple": "#C577DD",
"brightRed": "#DF6C75",
"brightWhite": "#FFFFFF",
"brightYellow": "#E4C07A",
"cursorColor": "#4F525D",
"cyan": "#0997B3",
"foreground": "#383A42",
"green": "#50A14F",
"name": "One Half Light",
"purple": "#A626A4",
"red": "#E45649",
"selectionBackground": "#BCFFC7",
"white": "#FAFAFA",
"yellow": "#C18301"
},
{
"background": "#002B36",
"black": "#002B36",
"blue": "#268BD2",
"brightBlack": "#073642",
"brightBlue": "#839496",
"brightCyan": "#93A1A1",
"brightGreen": "#586E75",
"brightPurple": "#6C71C4",
"brightRed": "#CB4B16",
"brightWhite": "#FDF6E3",
"brightYellow": "#657B83",
"cursorColor": "#FFFFFF",
"cyan": "#2AA198",
"foreground": "#839496",
"green": "#859900",
"name": "Solarized Dark",
"purple": "#D33682",
"red": "#DC322F",
"selectionBackground": "#FFFFFF",
"white": "#EEE8D5",
"yellow": "#B58900"
},
{
"background": "#FDF6E3",
"black": "#002B36",
"blue": "#268BD2",
"brightBlack": "#073642",
"brightBlue": "#839496",
"brightCyan": "#93A1A1",
"brightGreen": "#586E75",
"brightPurple": "#6C71C4",
"brightRed": "#CB4B16",
"brightWhite": "#FDF6E3",
"brightYellow": "#657B83",
"cursorColor": "#002B36",
"cyan": "#2AA198",
"foreground": "#657B83",
"green": "#859900",
"name": "Solarized Light",
"purple": "#D33682",
"red": "#DC322F",
"selectionBackground": "#FFFFFF",
"white": "#EEE8D5",
"yellow": "#B58900"
},
{
"background": "#000000",
"black": "#000000",
"blue": "#3465A4",
"brightBlack": "#555753",
"brightBlue": "#729FCF",
"brightCyan": "#34E2E2",
"brightGreen": "#8AE234",
"brightPurple": "#AD7FA8",
"brightRed": "#EF2929",
"brightWhite": "#EEEEEC",
"brightYellow": "#FCE94F",
"cursorColor": "#FFFFFF",
"cyan": "#06989A",
"foreground": "#D3D7CF",
"green": "#4E9A06",
"name": "Tango Dark",
"purple": "#75507B",
"red": "#CC0000",
"selectionBackground": "#FFFFFF",
"white": "#D3D7CF",
"yellow": "#C4A000"
},
{
"background": "#FFFFFF",
"black": "#000000",
"blue": "#3465A4",
"brightBlack": "#555753",
"brightBlue": "#729FCF",
"brightCyan": "#34E2E2",
"brightGreen": "#8AE234",
"brightPurple": "#AD7FA8",
"brightRed": "#EF2929",
"brightWhite": "#EEEEEC",
"brightYellow": "#FCE94F",
"cursorColor": "#C2C2C2",
"cyan": "#06989A",
"foreground": "#555753",
"green": "#4E9A06",
"name": "Tango Light",
"purple": "#75507B",
"red": "#CC0000",
"selectionBackground": "#90499E",
"white": "#D3D7CF",
"yellow": "#C4A000"
},
{
"background": "#000000",
"black": "#000000",
"blue": "#000080",
"brightBlack": "#808080",
"brightBlue": "#0000FF",
"brightCyan": "#00FFFF",
"brightGreen": "#00FF00",
"brightPurple": "#FF00FF",
"brightRed": "#FF0000",
"brightWhite": "#FFFFFF",
"brightYellow": "#FFFF00",
"cursorColor": "#FFFFFF",
"cyan": "#008080",
"foreground": "#C0C0C0",
"green": "#008000",
"name": "Vintage",
"purple": "#800080",
"red": "#800000",
"selectionBackground": "#FFFFFF",
"white": "#C0C0C0",
"yellow": "#808000"
}
],
"showTabsInTitlebar": false,
"theme": "system",
"themes": [],
"useAcrylicInTabRow": false
}

View File

@ -0,0 +1,169 @@
---
title: Alpine Linux Homelab
date: 2023-12-22
tags:
- computers
---
A few days ago, my root filesystem crashed.
This isn't as big a deal as it would have been before containers:
because the filesystem was happy enough that docker could start,
all of my services were able to run, since they live on another filesystem.
Weird.
But still,
it needed to be repaired,
so I could run things like `git` (which had become corrupted).
A few of my pals have been using Alpine Linux for a while.
I've used similar distributions--tinycore and OpenWRT--so
I sort of knew what Alpine was about.
And I've used Alpine a whole lot in container images,
because it's just so gosh darn small.
So I dove in on that.
2 hours later, I had a whole working system,
running my containerized services just like before.
This is an absolutely bonkers fast redeployment,
and I owe a lot of that to some clever design decisions
made by the Alpine developers.
Setup
-----
I've got Alpine booting on my Raspberry Pi in "diskless" mode.
That means it loads the entire operating system into RAM.
It then mounts my 12TB btrfs array,
which has all the containerized services,
and launches podman to start them all up.
If I need to make system-wide changes,
I can just edit files in `/etc` and run
`lbu commit`, which will pick up changes and write them out
as a tarball
to my normally read-only SD card.
This is a Big Deal when you're booting from an SD card,
because those things can't handle lots of writes.
I've also got a service running that copies the tarballs
onto the 12TB btrfs system,
so I should be able to get back online in under an hour if I lose this card.
I moved from Docker to Podman because the latter uses about half the space on the root filesystem.
That's important to me,
because the root filesystem is in RAM.
RAM Disk
--------
Running a production machine entirely from RAM may seem foolish,
especially on a Raspberry Pi.
But it's actually pretty svelte:
Filesystem Size Used Available Use% Mounted on
tmpfs 3.8G 108.8M 3.7G 3% /
That's right, the entire host OS,
including all the junk needed for networking and podman,
takes up 108MB of the 8GB on this system.
That's just 1.35%!
I haven't checked,
but I suspect that base Ubuntu uses at least 108MB more RAM than base Alpine,
due to the myriad daemons and services it launches.
Podman
------
At my previous job,
I played around with podman a little.
It is more or less a replacement for Docker,
although it doesn't have swarm mode.
I gave it a shot in this conversion,
after being irritated that Docker itself frequently used over 10% of the CPU,
and quite a lot of RAM.
It turned out that podman not only doesn't appear to show up in the CPU usage,
but it also takes about half the space on the RAM disk as Docker did.
Running podman as root just worked out of the box:
I had to create two files in `/etc` to map my uid and gid
to get it working for my user account,
but now that's working nicely too.
I'm not using podman pods, though.
I looked into this once,
and it seemed to require learning a new schema for all kinds of things,
and maintaining hundreds of lines of YAML.
I know runit and bourne shell pretty well,
and I know how to launch individual podman containers with a shared bridge network,
so I'm just doing that.
A Beefier System
----------------
All of my services are running in containers.
At some point I realized:
why not put my login shell in a container, too?
So I did.
My work building that is in my
[toolbox repository](https://git.woozle.org/neale/toolbox),
but the summary is that I'm running a fat Alpine inside my lean host Alpine.
It still uses lbu and apk for preserving state,
but I can stretch out and install bash, vim, and even emacs.
I have a gigantic magnetic disk behind it,
I don't have to worry about storage space used by the OS in this setup.
Every time the container starts,
it reinstalls all the packages I have set up,
from the cached package repository I bind-mounted into the container from the btrfs array.
If I install new packages,
or update existing ones,
that updates the cache.
The tooling to do all this is minimal.
I encourage you to
[check out the repository](https://git.woozle.org/neale/toolbox)
if you're interested.
Most of the work is in the
[init script](https://git.woozle.org/neale/toolbox/src/branch/main/sbin/toolbox-init),
and most of the time getting *that* working correctly
was just me realizing that Alpine already had mechanisms for everything I wanted.
Runit
-----
When I managed my own (tiny) Linux distribution,
I became a fan of Dan Bernstein's "runit" init system.
Alpine was happy to let me set that up,
even providing a little `init.d` script to have their built-in openrc start runit.
Because runit uses files and filesystem pipes,
I can `sv restart /host/etc/services/foo` inside my "fat Alpine" container,
to restart the foo service *on the host OS*.
This lets me use my fat editor on service files,
and restart things without hopping onto the host OS.
Closing
-------
Moving from Ubuntu to Alpine gave me the following benefits:
* Boot time is down
* Memory use is down (I think)
* CPU use is down, and as a corrolary, so is energy use
* Absolute certainty that my system configuration is preserved in backup,
because every time I boot, I restore the system (into RAM) from that backup
* I get to keep all my creature comforts like bash, btop, vim, emacs, and the like
* If my root filesystem ever crashes again, I can just make a new Alpine SD card,
copy a backup tarball onto it, and I get my system back
Thanks to Nick Moffitt for blabbing about Alpine enough to get me interested,
and for providing some setup instructions (which I eventually completely undid)
to get me started quickly!

15
content/blog/_index.md Normal file
View File

@ -0,0 +1,15 @@
---
title: Neale's Blog
---
Blog is short for "web log".
It's a sort of online journal,
that anyone who finds it can read.
Mostly when I write here,
I'm thinking the reader will either be someone looking me up online who wants to know more about me,
or someone in the future who wants to know what life was like during my time.
I don't have a lot of personal anecdotes,
or commentary on current events.
Mostly this is just hobbies and idle thoughts.

39
content/blog/new Executable file
View File

@ -0,0 +1,39 @@
#! /bin/sh
cd $(dirname $0)
case "$1" in
""|-*)
cat <<EOD 1>&2; exit 1
Usage: $0 SLUG [TITLE]
Makes a new blog page with the slug SLUG.
Also uses SLUG for the page title,
unless you provide TITLE.
You can, of course, change this later.
EOD
esac
SLUG="$1"
TITLE="${2:-$SLUG}"
YYYY="$(date +%Y)"
MM="$(date +%m)"
DD="$(date +%d)"
slug=$(echo "$SLUG" | tr 'A-Z ' 'a-z-')
dir="$YYYY/$MM-$DD-$slug"
index="$dir/index.md"
mkdir -p "$dir"
cat <<EOD >"$index"
---
title: $TITLE
date: $YYYY-$MM-$DD
tags:
- untagged
---
EOD
echo $index
vim + $index

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -0,0 +1,16 @@
---
title: ABQ Uptown
subtitle: Something From California Pizza Kitchen
date: 2021-04-02
---
![Salad with some sun dried tomatoes on it](image-1.jpg)
Steps:
1. Go to CPK
2. Order this bitchin' salad
3. Eat it
Review:
10/10.
Best salad I've ever had. My mom says it had roasted artichoke hearts. I still daydream about this salad. This was a good day.

Some files were not shown because too many files have changed in this diff Show More