Skip to main content
Paul Welty, PhD AI, WORK, AND STAYING HUMAN

Work log — 2026-03-25

What shipped

Two major themes: the learning feedback loop and the homepage overhaul. Both represent the product crossing a maturity threshold — from “functional tool” to “product that can be marketed.”

Skip tracking and the learning feedback loop. Implemented the complete article impression tracking system: frontend fires impression events when articles load (#462), the user_learn handler computes skipped articles by diffing impressions against engagements (#463), and a database migration adds the impression event type to the CHECK constraint plus a unique partial index for deduplication (#464). This closes the biggest gap in PRODUCT.md’s “learns you over time” promise — the system now observes the 90% of articles users see but don’t engage with, not just the 10% they explicitly vote on.

Homepage overhaul driven by Playwright review. Started with a cold honest assessment of the homepage via Playwright — wrote a detailed review to /tmp/eclectis-playwright-review.txt scoring it 7/10. The single biggest gap: no product screenshot. Executed 10 homepage issues in a single session: added a sample briefing preview card (#465, the #1 conversion fix), podcast as 4th output (#466), elevated learning loop messaging (#467), fixed scroll-reveal animations hiding content for reduced-motion users and crawlers (#468), replaced the generic hero headline with “All your sources. One briefing.” (#469), reframed free tier pricing to lead with value (#470), added persona-driven scenarios with competitor monitoring callout (#472, #473), and added footer links (#474). The remaining homepage issues (#471 social proof, #472 personas) were either shipped or moved to backlog as content decisions.

Production bug fix and hardening. Fixed a 500 error on briefing pages caused by isomorphic-dompurify using JSDOM which fails in Vercel’s serverless runtime. Swapped to sanitize-html (pure JS), then caught and fixed a regression where inline styles were being stripped (#475). Also fixed blog prose-invert making text unreadable in light mode (#476), added a branded 404 page (#477), fixed the error boundary hardlinking to /articles (#479), and moved a redundant import out of a loop (#478).

Briefing and podcast refinements. Changed the briefing article window from a fixed 48-hour lookback to “since last briefing” with a 24-hour minimum floor. This prevents repeated articles across consecutive briefings while ensuring regeneration always has content. Applied the same logic to podcast generation. Also improved podcast script prompts to name-drop specific authors, publications, and article titles.

Scout pipeline cleared. Ran two full scout passes (error handling, security, dead code, performance, UX, features) plus a targeted follow-up on the day’s changes. All findings were executed or triaged to backlog. The codebase is clean — dead code scan found zero unused exports, security scan found no critical issues.

Articles page UX. Separated the filter/sort controls into two labeled groups (#344) — filters (source, status, score) on the left, sort on the right, with clear “Filter” and “Sort” labels.

Completed

  • #344 — Confusing sort/filters (separated into labeled groups)
  • #457 — Raindrop/Readwise push handlers crash on network errors
  • #458 — Confirmation dialogs for passkey delete and integration remove
  • #459 — Passkey challenge cleanup error logging
  • #460 — Track article skips (decomposed → #462-464)
  • #461 — Full data export with error tracking
  • #462 — Track article impressions on /articles page
  • #463 — Incorporate skip signals into user_learn prompt
  • #464 — Impression event type and dedup index
  • #465 — Sample briefing preview on homepage
  • #466 — Podcast as 4th output on homepage
  • #467 — Learning loop messaging elevated
  • #468 — Scroll-reveal animation fix for reduced-motion/crawlers
  • #469 — Hero headline replaced
  • #470 — Free tier pricing reframed
  • #472 — Persona-driven scenarios added
  • #473 — Competitor monitoring use case named
  • #474 — Footer links (blog, privacy, terms)
  • #475 — Fix sanitize-html stripping inline styles
  • #476 — Fix blog prose-invert in light mode
  • #477 — Custom branded 404 page
  • #478 — Move urlparse import to module level
  • #479 — Fix error boundary hardlink to /articles

Also closed: #454 (Stripe env vars verified), #455 (false alarm), #376 (won’t fix — premise wrong), #77 (won’t fix — not reproducible)

Briefing/podcast fixes (no issue numbers): replaced isomorphic-dompurify with sanitize-html, changed briefing lookback to “since last briefing” with 24h min, improved podcast script name-dropping, matched podcast article window to briefing cutoff.

Carry-over

  • #471 — Homepage social proof (backlog — needs real users/testimonials)
  • #64 — Email inbox scanning for newsletters (backlog/discussion)
  • Next.js 16 canary Turbopack build crash persists locally. Vercel builds work fine.

Risks

  • Impression tracking volume — monitor engagement_events table growth. Unique partial index prevents per-article duplicates, but each page visit creates ~20 rows. First week of data will reveal the actual growth rate.
  • sanitize-html inline style permissiveness"*": ["style"] allows all inline styles. sanitize-html’s built-in CSS sanitization prevents expression() and other XSS vectors, but worth monitoring for edge cases in user-submitted content (though we don’t have any — all HTML is Claude-generated).

Flags and watch-outs

  • Homepage footer links to /privacy and /terms which don’t exist yet — they’ll 404 until placeholder pages are created.
  • The hero headline change (“All your sources. One briefing. Gets smarter every day.”) is a significant positioning shift. Watch for user/visitor feedback.
  • Briefing “since last briefing” cutoff: if the scheduler misses a day, the next briefing will have 48+ hours of articles. The 24h minimum floor prevents empty briefings on manual regen, but a missed schedule creates a larger-than-normal briefing.

Why customer tools are organized wrong

This article reveals a fundamental flaw in how customer support tools are designed—organizing by interaction type instead of by customer—and explains why this fragmentation wastes time and obscures the full picture you need to help users effectively.

Infrastructure shapes thought

The tools you build determine what kinds of thinking become possible. On infrastructure, friction, and building deliberately for thought rather than just throughput.

Server-side dashboard architecture: Why moving data fetching off the browser changes everything

How choosing server-side rendering solved security, CORS, and credential management problems I didn't know I had.

The work of being available now

A book on AI, judgment, and staying human at work.

The practice of work in progress

Practical essays on how work actually gets done.

Designed to learn, built to ignore

The most dangerous organizational failures don't throw errors. They look fine, return results, and quietly stay frozen at the moment of their creation.

The variable that was never wired in

The gap between having a solution and using a solution is one of the most persistent failure modes in organizations. You see the escaped variable. You see the risk register. You assume the work is done.

Your empty queue isn't a problem

Dropping a column from a production database is the organizational equivalent of admitting you were wrong. Five projects cleared their queues on the same day, and the bottleneck that emerged wasn't execution — it was taste.