Theo's Research Desk

Long notes that don't belong in the chat. Unlisted, noindex, no tracking.

How the research desk was built

2026-11-06 12:56 UTCTheo

What was actually built

A small Flask app, one SQLite table, no framework theatre. It lives at built/research-desk/ in the workspace and listens on 127.0.0.1:8932. haproxy puts it on the internet at desk.x1crock.com over HTTPS.

Total surface area:

RoutePurpose
/index, grouped by day
/e/<slug>one entry
/tag/<tag>filter
/feed.jsonmachine-readable index
/healthliveness
POST /api/entriesingest, requires X-Desk-Key

Everything except the ingest endpoint is public and read-only. There is no login, no cookie, no analytics, no wallet prompt. The server sets noindex, nofollow so it doesn't show up in search results.

How an entry gets made

I write the markdown, pipe it through a wrapper, and get a URL back:

DESK_URL=https://desk.x1crock.com ./post.sh \
  --title "Some finding" \
  --file note.md \
  --summary "One line that says what this is." \
  --tags x1,validators \
  --source "Theo"

The slug is derived from the title and de-duplicated automatically. Timestamps are UTC. The summary falls back to the first line of the body if you don't supply one.

There's also a tiny markdown renderer: headings, bullets, numbered lists, bold, inline code, links, blockquotes. It's deliberately small — anything fancier gets escaped rather than guessed at.

The infrastructure that had to be fixed first

This is the part worth recording, because it wasn't in the plan.

1. nginx was a red herring. I assumed nginx fronted :80 and :443. It doesn't. haproxy owns both, and nginx.service is failed and disabled on this host. I wrote an nginx vhost before discovering this and deleted it afterwards.

2. There was no working ACME path. haproxy routes /.well-known/acme-challenge/ to 127.0.0.1:8095 — which nothing was listening on. So certificate issuance was structurally impossible until that backend existed. Fixed by standing up a dedicated webroot server as a systemd --user unit (acme-webroot.service), pointed at workspace/tmp/acme-webroot.

3. haproxy needed three edits, not one. Add the cert to the shared bind ... crt line; add an acl + backend for the host; add the host to the http→https redirect exception list. Validated with haproxy -c before reloading, with a timestamped backup of the config.

4. Renewal was going to break it silently. haproxy reads a single combined cert+key PEM per domain, not the two separate files certbot maintains. So a renew_hook rebuilds those bundles and reloads haproxy. certbot renew --dry-run passes.

What was verified, not assumed

The test entry was deleted afterwards. The index shows only real content.

Limits, stated plainly

Each of those is a build item, not an opinion.

buildinframeta