david deleted branch dev at david/til

4 days ago

david pushed to main at david/sruc

1 week ago

david pushed to main at david/sruc

  • 51f99a393c wcag sweep: hidden h1s, empty headings, contrast ♿

1 week ago

david pushed to main at david/til

1 week ago

david renamed repository from srh to david/srh-old

1 week ago

david pushed to main at david/til

2 weeks ago

david pushed to main at david/sruc

2 weeks ago

david pushed to main at david/gw

3 weeks ago

david pushed to main at david/gw

  • 50408c04ca fix php8 fatal on profile - css colors must be countable

3 weeks ago

david pushed to main at david/dwp

3 weeks ago

david pushed to main at david/dwp

  • 0388ca3f94 fix PHP 8 deprecations in updater and mega nav walker Declare Updater::$ver explicitly instead of relying on a dynamic property (deprecated in PHP 8.2). Drop the default on $depth in WP_Bootstrap_Mega_Navwalker::display_element(), since the required $args and $output that follow it triggered the PHP 8.0 "required parameter after optional" deprecation.

3 weeks ago

david pushed to main at david/phf

3 weeks ago

david deleted branch master at david/phf

3 weeks ago

david pushed to main at david/phf

3 weeks ago

david created new branch main at david/phf

3 weeks ago

david pushed to main at david/kop

  • 94f747e3d8 Fix "Attempt to read property ID on null" in header.php $post is null in the template's global scope on non-singular views (404, empty search, some archives), so $post->ID raised a PHP 8 error before any header markup was emitted. Resolve the ID via get_queried_object_id() guarded by is_singular(), and only destructure wp_get_attachment_image_src()'s return when it actually gave back an array -- it returns false with no thumbnail. $width is initialized to 0 so the comparison is always defined. Singular posts with a large-enough thumbnail behave as before; every other view falls through to the default header_image() banner.
  • e4ad2f2cc1 Import pastry theme as deployed on kingsofpastry.com Twenty Ten-derived theme, imported as-is from production for tracking.

3 weeks ago

david created new branch main at david/kop

3 weeks ago

david created repository david/kop

3 weeks ago

david deleted branch master at david/hbr

3 weeks ago

david pushed to main at david/hbr

3 weeks ago

david created new branch main at david/hbr

3 weeks ago

david pushed to main at david/ralph

  • 79ff68de00 rag: name the site exports by source, tag every chunk with a kind sites/ held two directories named for two domains, davidawindham.com and davidwindham.com, which read as two different sites. They are not. Both are davidwindham.com content — the legacy WordPress posts and pages redirect there, which is why both entries already shared one baseUrl and one `daw` collection. The only thing the old directory name recorded was where that content used to live, and it recorded it in the one place that looks like a routing decision. Renamed to sites/davidwindham.com-wp/, so the suffix names the SOURCE (a WordPress export) rather than a domain that no longer serves it. The domain survives in a comment, where history belongs. Safe because per-file frontmatter `url:` is authoritative and all 470 files carry one — urlFor() never consults the path, so no citation URL depends on the directory name. Confirmed empirically: the wp rows re-ingested to 439 files / 1325 chunks, identical to before the move. Adds metadata.kind — `site` | `wp` | `til` — so a retrieved chunk is traceable to its source set without inferring it from a path. Collection alone could not do this: `daw` holds both the single-page site and the WordPress export. ingest-daw-til.mjs writes `til` to match; TIL keeps reading the live Docusaurus repo and gets no export dir under sites/, since a copy there would just drift. One migration hazard worth naming: ingest-site.mjs clears a site's old rows by directory prefix, and the new prefix does not match the old path, so a plain re-run would have left the 1325 pre-rename rows behind as duplicates rather than replacing them. They were deleted explicitly before re-ingesting. Index verified after: 5331 documents, 5331 vectors, no orphans, nothing left on the old path, no citation URL on the old domain. Also corrects three comments citing davidawindham.com/til, a path that never existed on that domain — the TIL site has only ever been davidwindham.com/til. Left alone deliberately: davidawindham.com is still live. code.davidawindham.com is the `code` git remote, and davo-bot.md's note that legacy subdomains remain is true. This was not a find-and-replace.

3 weeks ago

david pushed to main at david/til

  • 263736b002 docs(host): archive Discourse, Mailcow, NodeBB, Siege/Sproxy, Webmin, Zammad - new Server Hosted > Archive page listing them with full project URLs - remove the 6 pages - strip incoming links (sidebar, docs index, _host partial); repoint homepage log entries to Archive and the mermaid host node to Drupal
  • 76a318d678 invoiceplane
  • View comparison for these 2 commits »

3 weeks ago

david pushed to main at david/ralph

  • e4cc889424 tools: reshape the repo to match the model, add reminders-export The word "tools" was carrying three meanings: MCP tools, the tools/ directory of pinned installers, and the "one-off tools" the diagram used for standalone CLIs. Renaming rather than nesting resolves it: installers/ pinned installers for THIRD-PARTY binaries, landing outside the repo. One file, and "installer" is the word the README already used for it — a better name than tools/ ever was. tools/ standalone CLIs written here that produce files. Nothing is installed; you run them. Nesting the CLIs under the existing tools/ was the obvious move and the wrong one — it would have merged two things the README sells as different reuse channels. Adds tools/reminders-export: Apple Reminders -> markdown for the local chat. Shortcuts does the fetching (it holds the permission, and its Find Reminders filter UI beats anything scriptable); this owns the formatting, because the part worth versioning is the part you can diff. Output defaults to ~/.ralph/reminders.md — outside this repo and outside daw_til, since reminders name people, clients and dates and daw_til feeds a corpus a public widget quotes from. Deliberately NOT ingested into the RAG index, for two reasons. The index is a snapshot embedded at ingest and reminders change hourly, so "what's coming up" would be answered from stale vectors — and cited as confidently as anything else. And the query shape is wrong: "what's due Thursday" is a filter over structured fields, not a similarity search; a reminder titled "call the plumber" resembles neither the words nor the meaning of the question. The chat should read the file fresh per request instead. Three behaviours worth keeping: empty stdin refuses to write (an empty pipe means the Shortcut broke, not that you finished everything, and overwriting a good export with "Nothing outstanding." would hide that); writes are atomic; unparseable dates bucket under "No due date" rather than dropping the item. --self-test exercises all of it with no Reminders access. Diagram: "one-off tools/ per-project, not shipped" became false the moment reminders-export joined it — that one feeds Ralph, not a project. Relabelled tools/ standalone CLIs, with both CLIs under it, and the old node renamed installers/ so two boxes don't both read "tools/". Also: element indices are compared as STRINGS, so the sequence could not continue a1..a9 with a10 (which sorts before a9). ASCII puts digits before letters, hence a9 < aa < ab for the new elements. --validate caught it.

3 weeks ago

david pushed to main at david/ralph

  • 9386f636ab diagram: add VS Code as a client, reframe Ralph as the substrate "Ralph — personal assistant for projects" put the agent inside the architecture. It isn't: Ralph is the substrate (tools, index, context) and the agent sitting on top is swappable. Relabelled to say so, and added VS Code — the editor hosting Claude Code — as a client reaching in over MCP. That seam is the whole point. The valuable half of this repo (ralph-fs, the RAG stack, the corpus) speaks MCP, which Cline, Roo Code, Continue, Zed and Cursor also speak, so it is already portable. The Claude Code-specific part is plugins/ and the skills layer, and even there the diagram plugin's value is a standalone Python script. Drawing the client as a separate box makes "swap the agent" legible rather than unthinkable. Edited the .excalidraw JSON directly rather than regenerating: this canvas has been arranged by hand, the builder refuses to overwrite it, and arrow paths cannot survive a round trip through the spec. Every existing element keeps its exact coordinates — verified, 0 shapes moved. Two things worth recording about hand-editing this format: - Element indices are compared as STRINGS. The builder writes zero-padded a0001..a0090; elements added in the Excalidraw app get its own unpadded a1..a4, which sort *after* all of those. Taking max() over the set therefore returns 'a4', and continuing from it naively regenerates a0005 — colliding with elements that already exist. The new nodes continue Excalidraw's sequence (a5..a8) instead. `--validate` caught this. - An arrow needs its binding written from three places: startBinding, endBinding, and a boundElements entry on *both* shapes. Miss the last and the arrow stops following the boxes when they move. The tracked .excalidraw.svg is re-exported; its embedded scene decodes to the same 94 elements with every shape at its on-disk position.

3 weeks ago

david pushed to main at david/ralph

  • b34aceff4f chore: drop the orphaned diagram PNG, guard the deploy unit migration ralph-model.png had no consumer left — the README renders ralph-model.excalidraw.svg, which embeds its own scene and so serves as both the image and the editable source. 609KB of binary with nothing pointing at it. deploy.sh now asks the host what systemd unit it actually has rather than printing a note and hoping. ExecStart moved to dist/public.js when the servers were split; dist/index.js is now stdio-only and starts no HTTP listener, so restarting a host that still has the old unit takes davo-bot down instead of updating it. If the remote unit still mentions dist/index.js the script prints a blocking warning with the two commands needed first. Verified against all three states: warns on the old unit, silent on the new one, and silent when the host is unreachable (the check must never block a deploy just because ssh failed).

3 weeks ago

david pushed to main at david/ralph

  • fed8dd6d51 server: split davo-bot and the local chat into separate processes One Hono app on one port served both the public widget API and the local chat window, and src/index.ts started it alongside the stdio MCP transport. Three things that answer to different people were fused into one process. Two concrete problems, not just tidiness: - deploy.sh shipped the entire chat surface to the public host, including /api/conversations with GET, POST and DELETE, none of them authenticated. They were unreachable only because Apache proxies exactly two paths — a vhost edit away from exposing conversation history on a public box. - Every Claude Code session holding the MCP server also tried to bind :3001. EADDRINUSE was caught so nothing broke, but the chat window's availability depended on which process happened to win the port. Now: dist/public.js /api/ask, /ask/widget.js, /ask/demo Claude :3001 deployed dist/local.js /, /api/chat, /api/conversations* Ollama :3002 never dist/index.js MCP stdio tools none none src/rag/* stays shared; src/server/shared.ts holds asset loading, host/port binding and the retrieval helper. retrieveContext() now takes its collections as a required argument rather than defaulting, so each server states what it may retrieve at its own boundary — the public one passes PUBLIC_COLLECTIONS, and widening the retrieval default can never silently widen what it can quote. The local chat is Ollama-only by construction: the Claude option, streamClaude(), the model picker and its persisted preference are gone from ui.html. That server is where a private index belongs (point RAG_DB_PATH at one), and retrieval feeds whatever it finds straight into the generation request — a cloud model would transmit those chunks off the machine. The guarantee is a property of the process rather than a conditional inside it. Title is now "Ralph", since "Davo-Bot 2000" is the public widget's identity, not this one's. davo-bot behaviour is unchanged, and was verified rather than assumed: same port, same scoped CORS (allowed origin echoed, unknown origin refused), same rate limit, same 400s on empty/malformed/over-long input, same SSE ordering (sources -> text -> citation -> done). The chat routes now 404 there, and deploy.sh --delete-excluded removes the copies earlier deploys left behind. Deploy needs a one-time unit change: ExecStart moved to dist/public.js, so `systemctl daemon-reload` must precede the next restart or systemd keeps running dist/index.js, which no longer starts an HTTP listener at all. The ai.stu vhost now proxies :3002. Also re-tracks ralph-model.excalidraw.svg, re-exported from the current canvas. Its embedded scene decodes to the full 90-element document with every shape at the position it has on disk, so the one file both renders in the README and re-opens in Excalidraw as an editable drawing. The .excalidraw stays untracked. npm run public / local now detach properly (nohup + redirect; npm's sh waits on any child still holding stdout, which made `npm run servers` hang), and npm run servers / stop drive both at once.

3 weeks ago

david pushed to main at david/ralph

  • f80e4cb5d9 diagram: model Ralph as an assistant; untrack the working drawings Reworks the map from "what Ralph ships" to "what Ralph is". Context now flows in at the top and capability ships out below, as two halves of one assistant rather than two diagrams. Model changes: - Machines above everything: Stu, Ovid, Lisa, using the emoji from daw_til's docs/computers/*.md so they read the same way here as they do there. - Lisa is wired only to Calendar/Reminders/Contacts. It is an iPad; it carries the Apple apps but not the notes or the repos, and an earlier revision drew arrows claiming otherwise. Stu and Ovid share one sync row instead, which is both true and 8 arrows rather than 15. - mcp-server forks into ralph-fs (any project, clients included) and RAG + ask (daw + daw_til only). One box claiming "always loaded" to every project overstated RAG's reach: pointed at a client repo it searches a personal blog. The closed collection set is now stated on the edge. - one-off tools/ gets a parent above last-fm-snapshot; ai.stu / llama3.2 sits off to the left of Ralph, served from Stu. Untracks ralph-model.excalidraw and its .svg export. The canvas has been arranged by hand and is no longer regenerable from the seed, so its diffs are thousands of coordinate lines with no review value. Rooted patterns (/*.excalidraw) so plugins/diagram/examples/ralph-layout.excalidraw stays tracked — that one is a shipped worked example, not a working file. Both files remain on disk; git rm --cached was needed because .gitignore does not apply to already-tracked paths. Note the consequence: the hand-arranged layout now has no offsite copy, and it is precisely the part that cannot be rebuilt from the spec.
  • 9c60411053 diagram: add --watch, and refuse to clobber hand-edited drawings Two changes that belong together, because the first makes the second necessary. --watch rebuilds whenever the spec changes. It polls mtime rather than pulling in fswatch/entr/watchexec, keeping the script stdlib-only — no install step is the whole point of a python file in a skill directory. A spec that is briefly unparseable (any editor mid-save) logs and leaves the last good file in place instead of exiting, and each build lands via temp file plus atomic rename so an editor can never read a half-written diagram and render a blank canvas. That made an existing hazard much sharper. The .excalidraw is an artifact, but Excalidraw makes it inviting to edit: drag a box, save, and the drawing now holds layout the spec knows nothing about. Rebuilding destroyed it silently — demonstrated, not assumed: hand edit, touch the spec, edits gone, no warning. With a watcher running that can now happen while looking at another window. So the builder records a SHA-256 of what it writes (.<name>.buildstamp, gitignored) and refuses to overwrite a file that no longer matches. A digest rather than a timestamp: mtime misfires in both directions, since every build makes the output newer than the spec and a touch would forge innocence. Content comparison answers the real question — is this still the file I wrote? The refusal is not caution, it is a lossy round trip. Box geometry back-ports into a spec cleanly (x/y/width/height overrides land within 0.005px), but arrow paths cannot: Excalidraw recomputes them on drag, the builder recomputes them from node centers, and they disagree by up to ~167px — 299k differing pixels and a 1340 vs 1449px canvas. Once a diagram is arranged by hand the .excalidraw is the source of truth and the spec records the model, not the layout. SKILL.md says so, and says not to reach for --force to get past it. Verified: first build stamps; rebuilding its own output is allowed; a hand edit is preserved with an actionable message; --force overrides; a normal rebuild works again afterward.
  • 2d85803015 docs: correct the embedding model in code, UI, and CLAUDE.md The RAG index moved to in-process Transformers.js (Xenova/bge-small-en-v1.5, 384-dim) some time ago, but several places still described the pre-migration design where Ollama's nomic-embed-text did the embedding. That is not a cosmetic difference — it is the reason the hosted server needs no daemon. Measured rather than assumed: embed() returns 384 dims in-process; Ollama's nomic-embed-text returns 768. rag_embeddings is vec0(float[384]) and a vec0 dimension is fixed at create, so nomic vectors cannot physically be stored. Ollama is now used for exactly one thing — llama3.2 generation on /api/chat, reachable at ai.stu — and nothing reads nomic-embed-text at all. - CLAUDE.md described float[768]/EMBEDDING_DIM=768 and listed the embedding choice as OPEN. Rewritten to record what shipped, including the consequence that document vectors are baked into rag.db at ingest so the server only ever embeds the incoming question. - ui.html showed a "nomic-embed-text · hybrid" badge in the local chat window. User-visible and wrong. - ingest-dir.mjs justified its 2000-char cap with nomic's ~8192-token limit. bge-small truncates at 512 tokens, so the cap matters more than that implied. - retrieve.ts attributed the narrow distance band to nomic by name; the rationale holds for the current model, the attribution does not.
  • 3635c06c66 rag: keep generated scrobble snapshots out of the index last-fm-snapshot writes a weekly Last.fm digest into daw_til/notes, which the daw_til ingest would otherwise pick up on the next reindex. Those pages are generated data — tail-of-noise lists of artist and track names with no prose for a question to match against — and the corpus they would join is one a public widget quotes from. Nothing else filters them: they are published, not drafts, and not underscore partials, so they embed as cleanly as any note and simply dilute retrieval until one eventually surfaces as a citation for a question nobody asked. EXCLUDED_PREFIXES is matched per path segment, so "music/scrobbles" catches music/scrobbles/*.md without also catching a sibling like music/scrobbles-explained.md. Verified against six cases. Kept next to a comment pointing at last-fm-snapshot's SNAPSHOT_OUTDIR, since that coupling is what silently breaks if either side moves. Also documents the category the tool belongs to: a project-specific one-off is not a reuse channel, and the two are judged differently — a channel by what it costs every project that installs it, a one-off by whether its output lands somewhere appropriate.
  • View comparison for these 4 commits »

3 weeks ago

david pushed to main at david/til

  • 505da2dc39 docs(ai): sync ralph page from ralph README - pull latest README (Reuse-across-projects, marketplace, model diagram) - preserve ## Log section and the ralph-map video - MDX/Docusaurus fixes: self-close <br/>, absolutize repo-relative link, point ralph-model.png at /til/img/ - regenerate graph.json / recent.json
  • 78259e0479 ralph
  • 202d64b9ac srh
  • View comparison for these 3 commits »

3 weeks ago

david pushed to main at david/ralph

  • 2137f2c4d8 rag: namespace the index into daw + daw_til collections The RAG index backs a public, citation-emitting widget, so anything in it can be quoted to a stranger. It had no namespace: rag_ingest_directory pointed at any project wrote straight into that corpus, and retrieval had no way to tell site content from a scratch repo's notes. Two files had already leaked in this way (this repo's own README.md and CLAUDE.md, 35 chunks), which the widget could have cited. Adds a `collection` column with a deliberately CLOSED vocabulary — daw (davidwindham.com) and daw_til — rather than free-form per-project strings. There are exactly two publishable bodies of content; an ingest that cannot name one is a mistake, not a new namespace. Adding a third means editing collections.ts, which keeps it a review checkpoint. Both ingest tools now require a collection, as does scripts/ingest-dir.mjs (no default: it applies no draft filtering and attaches no citation URL, so a run against the wrong tree is exactly the case worth failing). Retrieval filters inside the retrievers — sqlite-vec's `rowid IN (...)` pre-filter and an FTS5 AND — not over their output. Post-filtering a global top-30 would starve the smaller collection whenever the other dominated the neighborhood; pre-filtering keeps a scoped search returning a full top-k. /api/ask and /api/chat pass PUBLIC_COLLECTIONS explicitly so the quotable set is stated at the public boundary. Unrecognized rows fail closed: retrieval names the collections it wants instead of excluding a blocklist, so a row matching neither rule is unreachable rather than deleted. The v2 migration leaves such rows at '' and logs a count, so a stray ingest surfaces for review; the two leaked files were then removed explicitly. rag_list_documents groups by collection and flags the '' bucket. The migration is gated on PRAGMA table_info, not user_version alone: a fresh database gets the column from CREATE TABLE while still reporting version 0, where an unconditional ALTER would fail. The backfill runs through collectionForPath() so the classification rule lives in one place — and matches case-sensitively, because SQLite's LIKE is case-insensitive and '%/sites/%' also matches /Users/david/Sites/daw_til/..., which would have filed every TIL note under daw. Verified against the live index: 743 documents classified 440 daw / 303 daw_til, no unclassified rows, no orphaned embeddings, FTS in sync; scoped searches return only their collection and still fill top-k; verbatim text from an unclassified row retrieves nothing; /api/ask still streams sources -> text -> citation with live /til/ URLs.
  • 78f7f8c788 diagram: add the Ralph reuse-model drawing to the README Draws the three reuse channels (marketplace, mcp-server, pinned installer) as one picture, generated from a seed spec via the diagram plugin. Ships three artifacts rather than one because each renders somewhere the others don't: .excalidraw the editable source .excalidraw.svg vector export with the scene embedded as a payload comment, so it re-opens in Excalidraw as a drawing .png what the README actually embeds The README points at the PNG because VS Code's markdown preview does not render SVG in <img> — a hard restriction that the workspace's markdown.preview.security.level=allowInsecureLocalContent does not lift. The PNG is 2x (1560px) so it stays sharp at any render width. Floated right with align="right": GitHub's HTML sanitizer strips style attributes, so the legacy align attribute is the only float that survives there. <br clear="all"> keeps the next heading from riding up alongside.
  • View comparison for these 2 commits »

3 weeks ago

david pushed to main at david/ralph

  • 362fbbdd22 diagram: named themes, font selection, contrast-aware labels Style was four values sitting in one spec file, so nothing carried to the next diagram. It's now a `theme` key in the generator plus a house-style default in SKILL.md, which is what makes it a preference rather than a one-off. - theme: slate (Nunito, #ffffff stroke, #484c57 canvas, no fills) and sketch (Excalifont on white, accent fills). Explicit spec keys override the theme. SKILL.md instructs slate by default for anything being shown to other people. - font: named faces resolved against Excalidraw's FONT_FAMILY enum, checked against upstream rather than memory — 4 is deliberately unused there, and Lilita One / Comic Shanns are 7 / 8, which is easy to transpose. Numeric ids pass through; unknown names error with the valid list. - stroke / background / fills for one-off overrides. A light stroke on the default white canvas would be invisible, so background darkens automatically unless set. - Bound labels darken when a light stroke override would otherwise put pale text on a pale accent fill. Inert with fills off, but stops the next dark-themed diagram walking into it. Collapsing ralph-layout's spec to `theme: slate` reproduces its .excalidraw byte-for-byte, so that part is purely a refactor of where the values live. Drops the davo-bot-flow example (recoverable from 68c6fff) and repoints the README's usage snippet at ralph-layout. Note no example now exercises the light-canvas sketch theme.

3 weeks ago

david pushed to main at david/ralph

  • 6601e21c5e docs: document reuse across projects, add a layout diagram README gains a "Reuse across projects" section up front, since that is what this repo is organized around. It states the thing that is easy to assume and wrong: reuse is not filesystem inheritance. Sitting above Sites/ gives a project nothing — CLAUDE.md, .claude/settings.json, and .mcp.json are all scoped to their own directory. Capabilities travel through one of three channels (plugin marketplace, user-scope MCP server, pinned installer), tabulated with what each costs per session and when to reach for it. Also adds plugins/diagram/examples/ralph-layout.* — the repo drawn as those three channels rather than as a directory tree, since `ls` doesn't explain why tools/ and plugins/ are different kinds of thing. Doubles as a second worked example for the diagram skill. Fixes a stale #codebase-graph-codebase-memory-mcp anchor left by a heading rename; all internal anchors now resolve.

3 weeks ago

david pushed to main at david/ralph

  • 68c6fff470 plugins: Excalidraw diagram skill, and make this repo a marketplace .claude-plugin/marketplace.json turns Ralph into a plugin marketplace, so work developed here installs into other projects. A skill costs nothing until invoked, unlike an MCP server — codebase-memory's schemas measure ~6.5k tokens per session, against ~20.5k for the whole of mcp-server/src. plugins/diagram generates editable .excalidraw files from a compact nodes/edges spec, for explaining a system to people without the code in their head. No Excalidraw Plus account, API key, or network access — it writes files. The script owns the schema rather than the model emitting JSON: elements carry ~25 fields plus two-way refs between shapes, labels, and arrows, and a missing backlink yields a file that opens blank or unlabelled. Two failure modes found while testing, both of which render wrong rather than erroring: - Excalidraw compares `index` as a STRING, so unpadded values sort a10 before a2 and z-order scrambles past nine elements. Now zero-padded, and validated. - Container/label bindings must point both ways or the text silently vanishes. --validate checks referential integrity and index ordering, and the builder refuses to write a file that fails it; negative-tested against four deliberate breakages. Output is deterministic (seeds hash from node ids), so regenerating an unchanged spec is byte-identical and edits diff cleanly. examples/ holds the davo-bot request flow, phrased for a non-technical reader.

3 weeks ago

david pushed to main at david/ralph

  • d41713985c tools: pinned installer for codebase-memory-mcp Shared code-graph MCP server, installed from this repo but usable from any project. Only the installer is version-controlled — the binary is a C static build from GitHub Releases and nothing here links against it. - tools/install-codebase-memory.sh pins v0.9.1-rc.1, verifies SHA-256 against checksums.txt and Sigstore provenance via `gh attestation verify` (a FAILED attestation is fatal; a missing gh is not), ad-hoc-signs on macOS, smoke-tests --version, and prints the registration command. --ui installs the variant that embeds the 3D graph frontend. - Changes no config, unlike upstream's installer, which rewrites MCP client config across ~43 client surfaces including ~/.claude.json. Updating means bumping VERSION here, in git. - Binary targets ~/.local/bin, not this repo: a user-scope registration is one absolute path read from every project, so it must survive moving this repo. - README documents install, cross-project use, CLI testing without registering anything, the UI (daemon owns it; --ui=true only persists config), and where the tool does and doesn't help — verified on ralph (469 nodes, accurate search) and daw_til (3474 nodes, search_graph returns 0 because the graph is 2541 markdown Sections; that corpus is the RAG index's job).

3 weeks ago

david pushed to main at david/ralph

  • a6f4cf6a3b docs: reframe intro as parent MCP server, restore env-var docs The intro now describes Ralph as the parent MCP server other projects wire into, rather than a Claude Code sandbox. Also restores the path-safety and configuration docs from e52125f that an editor buffer overwrote before that commit landed: the RALPH_ALLOWED_DIRS / RALPH_ROOT_DIR table, the .mcp.json env block, the allowlist-widening tradeoff, and the user-scope registration for using ralph-fs elsewhere. Those sections still described a hardcoded allowlist the code no longer has.

3 weeks ago

david pushed to main at david/ralph

  • e52125fa06 mcp: env-driven path allowlist, decouple ROOT_DIR from the RAG index Make ralph-fs usable from projects other than this one. - ALLOWED_DIRS now reads RALPH_ALLOWED_DIRS (PATH-style list); ROOT_DIR is its own RALPH_ROOT_DIR rather than ALLOWED_DIRS[0], so widening the allowlist no longer moves relative-path resolution or the RAG db. - Pin RAG_DB_PATH in .mcp.json: the index is a fixed corpus (davidawindham.com + daw_til) backing the ask widget, not per-project state. - Case-insensitive prefix test on darwin/win32 to match the filesystem. - Fix search_files dropping every hit from a non-primary allowed dir: it filtered on startsWith(ROOT_DIR) instead of the allowlist. - README: env var table, cross-project registration, storage rationale, and a ralph-loop image above Features.

3 weeks ago

david pushed to main at david/gw

4 weeks ago

david pushed to main at david/gw

4 weeks ago

david pushed to main at david/til

4 weeks ago

david pushed to main at david/til

1 month ago

david pushed to main at david/daw

  • 9d0f638619 Posts: sticky posts on page 1 only + 100px between posts - A custom Query Loop block (inherit:false, e.g. /desk/posts) prepends sticky posts to EVERY page because the block query never sets ignore_sticky_posts. Add a query_loop_block_query_vars filter that, on pages after the first, drops the stickies + sets ignore_sticky_posts — pinning them to page 1 only. Scoped to custom queries so the inherit:true search/archive blocks are untouched. - 100px spacing between the full posts on /desk/posts (body.page-id-3919).
  • 788307c941 SEO: add BlogPosting + BreadcrumbList JSON-LD Only Person + WebSite identity JSON-LD was emitted site-wide; posts/pages had no per-content schema (the one BlogPosting was microdata inside the now-removed dead scroll template). Add to inc/seo.php on wp_head (so it covers the block templates): - Person gets a stable @id (#person). - BlogPosting on single posts (headline, datePublished/dateModified, author + publisher -> the Person @id, image, description). - BreadcrumbList for hierarchical pages, posts (Home > category > post), and term archives.
  • 2eb4a8077c Decruft: remove the dead backbone infinite-scroll The underscore.php/underscore-archive.php client-side templates + js/loop.js (a backbone/underscore/wp-api 'load more' scroll) never ran — loop.js appends into getElementById('main'), which no element has, and underscore-archive.php wasn't included anywhere. Remove the templates, loop.js, the _s_backbone-loop enqueue + settings localization + the two _s_backbone_get_request_* helpers, and the get_template_part('underscore') in page-desk.php. Desk's posts are a static get_posts list; nothing visible depended on this.
  • View comparison for these 3 commits »

1 month ago

david pushed to main at david/daw

  • ca44eac708 Studio: fix spelling + tidy the Projects pane copy

1 month ago

david pushed to main at david/daw

  • d892d2771b Analytics: card layout, flat page report with /til subpages - Restyle the Analytics page to match the other card pages: .container.leader with a white title above a #dadada #content card on the dark body (box-shadow:none kills the legacy article.single shadow), map moved inside the card, transparent list-group rows. - Content column: Matomo flat=1 so /til subpages (tracked via docusaurus-plugin- matomo on the same idSite=1) list as individual pages instead of one 'til' folder; links point at davidwindham.com; the bare '/' homepage row shows '/index' so it's clickable. Bump both Recent Visitors and Content filter_limit to 50.

1 month ago

david pushed to main at david/daw

  • 6fec37b165 Studio: contain hero slides as uniform cards, Craft tab, action buttons, Design laptop - Contain every hero slide in a content-width .dw-studio-stage card (rounded top, overflow-clipped for video, per-slide bg on the card not the full-width stage) so each slide reads as the top half of the .studio-lower content card, flush with it — cohesive with the other pages. Inner slide .containers fill the stage (no fixed 1170 width) so content no longer overflows the right padding. - Uniform slide height via min-height: max(calc(40vh - 100px), 440px) so short slides fill to the tallest (Craft fact slides / Projects cards) instead of each sizing to its own content. Added top/side content padding to the card. - Rename the Wonder tab to Craft: it still auto-rotates the fact slides once, but now stops on the last and keeps its new #wonder copy pane active (placeholder to edit). - Action buttons bottom-right: 'Art Projects ->' (/studio/art) on the Art pane, and a right arrow added to the Web 'Learn More ->' button. - Design slide: studio-design-3.jpg seats on the laptop (svg-mac) screen via an inline <image> in viewBox coords, fading in after the DrawFillSVG stroke draw (~2.4s); a bit more top room above the laptop. (img/ assets are gitignored — deploy separately.)

1 month ago

david pushed to main at david/daw

  • 4e16705723 Studio: Recent Projects slide, Web/UX merge, action buttons, slate duotone hero - Projects tab is now in-page: the top slide is a #dadada card titled 'Recent Projects' holding the 4 latest studio-category posts as cards, styled as the top half of the .studio-lower content card (rounded top, flush with its square top, transparent hero stage so the slate body shows around both). Copy + a 'View all projects' button sit in the pane below. - Merge Web + UX into one Web/Dev slide and remove the old Web slide, renumbering all the slide-array indices studio.js relies on (tab data-slide, CONFIG, WONDER). studio.js: split activate() into setHero() (carousel) + setActiveTab() (tab/pane matched by href→id) so a content-only tab like Projects can move independently. - Every actionable item now sits bottom-right of the content card: 'Learn More' (Web/Dev, was the inline Capabilities link) and 'View all projects' (Projects), via .dw-studio-action absolutely positioned in .studio-lower. - Remove the DRAFT admonition + its now-unused .dw-admonition* styles; normalize the Studio tab copy padding to match the other tabs (2.5rem). - Tint the Studio hero image to the slate blue: a 'Slate' duotone preset in theme.json + a self-contained inline SVG filter (#dw-duotone-slate, #484c57 → #cfd2d8), applied to the first slide only via CONFIG[0].filter.

1 month ago

david pushed to main at david/daw

  • 44b3b9090f 404: add spacer blocks for breathing room around the search form

1 month ago

david pushed to main at david/daw

  • 91f96049ca Search: merge TIL (Docusaurus) results + two-column highlighted layout - Add a 'TIL' column to the WP search page backed by the Docusaurus site's own prebuilt lunr 2.3.9 index. js/til-search.js loads /til/lunr-index.json + /til/search-doc.json and queries them exactly like docusaurus-lunr-search's adapter (same tokenizer separator, term boost, trailing wildcard), rendering a match-centered, <mark>-highlighted snippet list into #dw-til-results. lunr is vendored (js/vendor/lunr.min.js). Enqueued only on is_search(). Self-contained/removable: delete the two js files + the marked blocks in functions.php, templates/search.html, and style.css. - Lay the WP results and TIL results side by side (flex; search box full-width on top, columns stack on mobile), drop the 'From TIL' heading, and tighten the WP result rows to match the TIL rhythm. - Highlight the search term in the WP results too: dw_search_excerpt_highlight hooks render_block_core/post-excerpt (after the block's wp_trim_words strips tags) to swap in a match-centered snippet with <mark> terms. Server-side and independent of the TIL feature; excerpts only, never titles. Styled by body.search mark.

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/daw

  • 55bd2c459b Music: red badge counts + outline action buttons with green depress hover - .badge: color #fff -> var(--wp--preset--color--link,#67000a). Bootstrap badges had a colored bg; without it the white text was near-invisible on the light .list-group-item. Fixes the music last.fm counts and analytics hit-counts. - Action buttons (.daveo-radio-btn + .btn-dark): drop the solid dark fill for a WP-style outline in the site dark blue (slate-deep #484c57), tighter 4px radius, and a subtle raised drop-shadow that presses down on hover (translateY + shadow collapse) while the border/text/icon turn the footer link-hover green (#25c2a0). Search submit (in .input-group) opts out so it stays joined to the field.

1 month ago

david pushed to main at david/daw

  • d32404afe5 Date archives: classic date.php with card + year nav - Add date.php (mirrors category.php) so year/month/day archives get the #dadada content card and white title-above, instead of falling through to the bare block templates/archive.html (which had no background). Classic date.php outranks the block archive template in the hierarchy, like the existing category.php/tag.php. - Restore the year-to-year nav removed with the old archive.php: prev/next year via get_year_link(), based on the queried year, next hidden when it'd point at a future archive. Styled .dw-year-nav (white links on the dark archive body). - Finish neutralizing the legacy single-article shell on archive/music: add box-shadow:none + zero the bottom padding so the article.single drop-shadow no longer draws a faint border below the card. - page-archive.php: date format m/d/y -> y/m/d, matching the desk listing.

1 month ago

david pushed to main at david/daw

  • 770126e352 Desk: add Recent Photos Lychee stream, remove dead photo() scraper - Enqueue the Lychee embed CSS/JS in dw_scripts() scoped to is_page('desk') (the branch also matches archive/search, hence the guard). - New 'Recent Photos ( all @ /photo )' row above Docs/Notes/Lists: 10 photos laid out 2 rows of 5 (target col/row 175px), embed's own header suppressed (data-header-placement=none) so the desk-style h4 is the only heading. - Remove the dead photo() scraper from inc/utils.php — no callers, still pointed at the old photo.davidawindham.com domain, superseded by the embed.

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/daw

  • 7f144ce7a0 Mobile CSS: right-align navbar title The flex navbar's single .container has no fixed width on mobile, so it shrinks to the title's width and its auto margins center it — centering 'David A. Windham'. Force .navbar .container to full width below 768px so float:right pins the title to the right edge, matching desktop.

1 month ago

david pushed to main at david/daw

  • e3a2c0b32a Mobile CSS: fix narrow content, missing backgrounds, horizontal scroll - Content cards (classic + block) had a hardcoded 96px (1in) padding with no mobile override, leaving a ~150px content column on phones. Drop all cards to 1.5rem below 768px. - Remove the stale @media(max-width:667px) .single { background:none!important; box-shadow:none!important } that wiped the dark body bg off single posts on mobile (leftover from the old white-article design). - .leader zeroes horizontal padding, so on mobile bare .row negative margins bled past the viewport and cards pinned to the screen edge. Add 15px gutters to .container.leader on mobile so the dark body frames the card as on desktop. - Add overflow-wrap:break-word to content so long URLs can't force horizontal scroll on block pages (they don't inherit the .container word-wrap). Verified 0px horizontal overflow on single/contact/music at 390px.

1 month ago

david pushed to main at david/daw

  • cab3f6f5fa Bookmarks: point pull functions at davidwindham.com Move the three Shaarli fetch helpers (bookmarks/bookmarks_pull/bookmarks_all) from davidawindham.com/bookmarks to davidwindham.com/bookmarks, following the domain migration.

1 month ago

david pushed to main at david/dw

  • e22624a5c1 Work: Management list — alphabetize, add Server Maintenance + User Support, drop Social Media What? > Management: removed the 'I do not manage Social Media' line, sorted the list alphabetically, and added 'Server Maintenance / Upgrades / Support' and 'User Support / Training'. Rebuilt app/ (gulp build + html).

1 month ago

david pushed to main at david/daw

  • 6d376aa36a Studio: DRAFT admonition, vertical nav underline, Wonder tab, tab cleanup - DRAFT admonition (Docusaurus danger style) in the Studio tab above the copy: site link-color red fill + lighter-red left sliver + white copy/flame, full width, pulled up tight under the slider (scoped to the Studio tab only — other tabs keep their top padding). - Vertical 'magic line' underline on the tab subnav (the navbar/footer slider, keyed to the .active tab via MutationObserver). - Subnav: de-italicized, added a 'Wonder' item, and made 'Projects' a plain list item (dropped the button + its orphaned CSS). - Carousel: autoplay off (advances only on subnav clicks); 'Wonder' auto-rotates the 3 fact slides then returns to Studio and stops. - Copy: collapsed double <br><br> to single line breaks.

1 month ago

david pushed to main at david/daw

  • 337325a2bb Block templates: page title above the card + consistent top spacing - search/404/page.html: move the title above the #dadada card (white on the dark body), matching the classic archive/contact pages. Only single posts keep the title inside the content area. - 404 gets the dark body + #dadada card it was missing. - main.wp-block-group top padding clears the float:right .site-title header, so the block pages line up with the classic .container pages (they rode up next to the collapsed header before). - Block query pagination (Feed/Posts, search): center it and swap the text prev/next for <- -> arrows, so it matches the classic dw_paging_nav pagination.

1 month ago

david pushed to main at david/daw

  • a4ca9b59a4 Styling: art page dark theme, search card, pagination; drop legacy CSS - Art page: left-nav + Art Projects heading in #dadada, cards #dadada w/ small radius, removed the .art-gallery / #gallery-wrap dark backgrounds and the carousel arrow circles - Search: give the block results the same #dadada card as the archives - Pagination: centered, de-Bootstrapped pill links for classic + block archives - Remove the conflicting legacy .pagination block (red 27px misaligned arrows)
  • 1c1355bb93 Archives: title above the card + clean pagination markup - category.php/tag.php now mirror page-archive.php: white heading above the #dadada card (was buried inside via header.page-header), like the other pages - category page shows the excerpt + date to match tag - dw_paging_nav: drop the Bootstrap <ul>/justify-content-center wrapper and the &nbsp; padding around the arrows; emit a plain <nav class=pagination>
  • 228769aeab Sitemap: add npm + huggingface, 3-col Elsewhere, drop first amazon - Add npmjs.com and huggingface.co profiles (rel=me), alphabetized - Split Elsewhere into three balanced columns (9/9/8) - Remove the amazon gp/profile link (kept the wishlist)
  • View comparison for these 3 commits »

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/daw

  • f6cd56ae95 Sitemap: delink twitter, rebalance Elsewhere columns - Delink twitter (fix stray </a> left from the manual edit) - Move medium to the left column; drop youtube/user so the columns balance

1 month ago

david pushed to main at david/daw

  • 345981255a Sitemap: page-format background + dedupe, alphabetize, prune links - Give the sitemap page the dark body + #dadada card + white title like the contact/archive pages (body.sitemap rules in style.css, rebuilt v4-style.min.css) - Consolidate the Sub Domains section into the primary list; drop hr dividers - Remove dead links (Bio, CV) and duplicate targets so it's one URL per page - Alphabetize each grouped line and the Elsewhere columns - Elsewhere: prune old struck accounts, strike/delink coinbase/angel/opensea/ spotify, add struck twitter; drop peoplespants + first amazon
  • 59a3fa378d SEO: build meta description without rendering dynamic blocks dw_seo_description ran the full the_content filter to build the excerpt, which re-rendered the Gravity Forms block during wp_head (wasteful + tripped a GF Post-Category PHP 8 warning). New dw_seo_excerpt_from_content strips dynamic blocks via excerpt_remove_blocks before do_blocks.
  • View comparison for these 2 commits »

1 month ago

david pushed to master at david/radio

  • 2d7d511051 Player: current-playlist label + off-air button/gif state Current playlist/set name from the stream metadata: /api/status now returns the Icecast mount `genre` (set in the broadcast, e.g. "June 2016"), shown as a small "♫ <genre>" label under the now-playing title. Hidden off-air and when blank. Off-air state when the audio stream stops: previously only the tower/eq gifs swapped to their off art while the play button sat on "playing" over dead audio. Now the button also resets to paused (stopping the stalled audio) and dims to a greyed, not-allowed look; it re-enables when the stream returns. The click interceptor is refined to allow a *pause* while offline -- it only blocks attempts to *play* into a stream that isn't there -- which is what lets the reset stop playback without popping the connection dialog. Verified across online -> stopped -> back-on-air transitions: button paused + dimmed and gifs off when stopped, both restored when live.

1 month ago

david pushed to master at david/radio

  • 424cdb6a88 Audio autoplay (probe + first-gesture) and recent-tracks polling Autoplay, done honestly. Browsers block audio-with-sound autoplay on a cold visit (granted per-origin via Media Engagement Index), and Amplitude v2 doesn't check the play() promise, so a blind attempt would show "playing" with no sound. Instead: - On load, PROBE: play a tiny silent clip. Autoplay permission is per-page, so if the probe plays the real stream will too -- then click play. If blocked, leave it paused. Never fakes the state. - Fallback: start on the first user gesture anywhere (a real activation the browser always accepts), so it's never more than one click away -- guarded so clicking the play button itself doesn't double-toggle. - Gated on Amplitude being bound (window.load) to avoid clicking before its handler exists, which would silently no-op and never retry. Recent tracks now refresh every 2 minutes, REPLACING the list rather than appending (the old code would have stacked duplicates). Only the first load fades in; refreshes swap items silently. 120s poll vs the 60s /api/lastfm cache means every poll gets fresh data. Values go in as text nodes now, so a track or artist name can't inject markup (also closes a latent unclosed-span bug).
  • 605000d886 Front-end: video panel markup/styles, mellow cards, banner spacing - The "Video Stream" collapsible bar markup (index.html) and its styles (main.css): always-visible bar, red/grey status dot, chevron, 16:9 responsive video box, corner mute toggle, offline placeholder. - Audio player + chat cards recoloured from stark white to #dadada (the same grey the shared footer uses), so the page reads as one system. - Removed the padding between the Daveo Radio banner and the players/video (margin, not padding -- a background image paints across the padding box). - Includes hand edits to index.html.
  • 7d4ec08deb Add the-ham.org live video panel A live video panel alongside the audio, sourced from the-ham.org (nginx-rtmp fed by OBS over RTMP, served as HLS). - /api/live (app.js) proxies the-ham's stat.xml -- which, unlike the .m3u8/.ts, has no CORS -- finds the publisher under the `live` app, and returns {online, name, hlsUrl, width, height}. Config via HAM_STAT_URL / HAM_HLS_BASE / HAM_APP. Gives the client a ready HLS URL and a clean offline state without hardcoding a stream key. - src/js/ham.js: a persistent "Video Stream" bar (dot grey idle / red-pulsing live, chevron collapse/expand). Going live auto-expands and autoplays muted (muted is the only autoplay browsers allow); the corner button unmutes; collapsing tears the video down. - hls.js is vendored from npm but emitted as its own app/js/hls.min.js and lazy-loaded by ham.js only when video first plays -- NOT in the main bundle, so the common idle case pays nothing for it. Safari plays HLS natively. - Low-latency player config (lowLatencyMode, liveSyncDurationCount: 2, maxLiveSyncPlaybackRate) to pair with short server segments. Deploy note: this adds app/js/hls.min.js (a new file to upload) and hls.js as a dependency (server needs npm ci --omit=dev again).
  • View comparison for these 3 commits »

1 month ago

david pushed to main at david/ralph

  • 74b3815ae8 migrate content URLs from davidawindham.com to davidwindham.com Posts/pages rerouted to davidwindham.com; TIL already on davidwindham.com/til. Point ingest scripts at the new host, fix dead apex links in index.md, and update the persona doc (radio/chess now subdirectories; code.davidawindham.com git host retained). rag.db regenerated separately.

1 month ago

david pushed to master at david/radio

  • 2ff267c4f3 gitignore the _claude/ notes dir instead of the old root file

1 month ago

david pushed to _old at david/chess-io

  • 19ddccc66e gitignore _claude/ (local working notes, kept out of the public repo)
  • 3594b58d59 Add pm2 ecosystem config (fork mode, BASE_PATH=/chess) .cjs because the package is ESM. Single fork instance, not cluster: the in-memory games state can't be split across workers.
  • 69e1ee7268 deploy: document both websocket-proxy paths (2.4.47+ vs mod_rewrite) The old chess.davidwindham.com subdomain vhost used the pre-2.4.47 mod_rewrite idiom to proxy the socket.io websocket upgrade. Document that as Option B (adapted to the /chess subpath: match ^/chess/socket.io, keep the prefix) alongside the Option A upgrade=websocket one-liner, and note the old-subdomain redirect.
  • fbd2add631 Add production deploy examples (Apache reverse proxy + systemd unit) - deploy/apache-chess.conf.example: ProxyPass /chess to node on :8181 with upgrade=websocket, mirroring the /radio wiring. Goes in the existing davidwindham.com vhost. - deploy/chess-io.service.example: systemd unit running the app with BASE_PATH=/chess and TRUST_PROXY=1 (no DAW_ORIGIN in prod).
  • 35cab2594b Merge remote-tracking branch 'origin/_old' into _old # Conflicts: # README.md # public/javascripts/app.js # public/javascripts/socket.io/index.js # public/javascripts/socket.io/logger.js # public/javascripts/socket.io/manager.js # public/javascripts/socket.io/namespace.js # public/javascripts/socket.io/parser.js # public/javascripts/socket.io/socket.io.js # public/javascripts/socket.io/socket.js # public/javascripts/socket.io/static.js # public/javascripts/socket.io/store.js # public/javascripts/socket.io/stores/memory.js # public/javascripts/socket.io/stores/redis.js # public/javascripts/socket.io/transport.js # public/javascripts/socket.io/transports/flashsocket.js # public/javascripts/socket.io/transports/htmlfile.js # public/javascripts/socket.io/transports/http-polling.js # public/javascripts/socket.io/transports/http.js # public/javascripts/socket.io/transports/index.js # public/javascripts/socket.io/transports/jsonp-polling.js # public/javascripts/socket.io/transports/websocket.js # public/javascripts/socket.io/transports/websocket/default.js # public/javascripts/socket.io/transports/websocket/hybi-07-12.js # public/javascripts/socket.io/transports/websocket/hybi-16.js # public/javascripts/socket.io/transports/websocket/index.js # public/javascripts/socket.io/transports/xhr-polling.js # public/javascripts/socket.io/util.js # public/stylesheets/.sass-cache/5b513b16477b3edd25e02b0abdfc163a9034b078/stajl.scssc # public/stylesheets/style.scss
  • View comparison for these 6 commits »

1 month ago

david pushed to master at david/radio

  • 89e647bb2f Move radio to port 3002 (3000 is Gogs) Gogs (code.davidawindham.com) already listens on 3000 in production, so the app crash-looped with EADDRINUSE. 3001 is the ask/ralph server and 8181 is chess, so 3002 is the next free slot. Updated ecosystem.config.cjs, the Apache proxy example, the systemd unit, and .env.example. Verified end-to-end on 3002 locally through Apache at daw.stu/radio: health OK and the socket.io websocket upgrades.

1 month ago

david pushed to master at david/radio

  • 27e2aecc80 Rework deploy docs for FTP sync instead of git pull The server gets code by FTP, not `git pull`, and the process is managed over SSH (pm2/systemd). That flips the build story: build LOCALLY (your machine has the devDeps), then upload the result. Because app/ is gitignored, it only reaches the server if you build first and include it in the upload -- forget, and the server serves stale files with no error. - README: rewritten around FTP. What to upload (app/ + app.js + package files), what the server needs (npm ci --omit=dev for the 5 runtime deps only, no build tooling), and the redeploy loop (build -> FTP -> restart). - Flagged the FTP-doesn't-delete trap: removed files (gulpfile, webrtc bundle, vendored libs) linger on the server; inside app/ a stale bundle could be served, so clear the server's app/ before uploading a fresh one. - ecosystem.config.cjs / radio.service.example: same correction, git-pull lines replaced with the local-build + FTP + restart flow.

1 month ago

david pushed to master at david/radio

  • e47a39184d Add pm2/systemd deploy setup, mirroring chess-io Production runs this as a long-lived node process behind Apache at davidwindham.com/radio, same shape as /chess and /ask. Mirrors the chess-io convention so the two deploy the same way. - ecosystem.config.cjs pm2 config. Fork x1 (in-memory chat state; scale needs REDIS_URL + a shared adapter, not just more workers). Operational env inline; the one secret (LASTFM_API_KEY) comes from a gitignored .env via --env-file-if-exists. DAW_ORIGIN deliberately omitted. - deploy/apache-radio.conf.example the ProxyPass block for the existing davidwindham.com vhost. Option A (2.4.47+, upgrade=websocket) and Option B (older, mod_rewrite). - deploy/radio.service.example systemd alternative to pm2. - deploy/README.md first-time setup + redeploy runbook. The one difference from chess: this app has a build step. app/ is gitignored and compiled from src/ by esbuild, so prod must `npm install && npm run build` (full install -- the build needs the devDeps) before starting, and on every pull. Called out in all three deploy files. Auth is intentionally absent and the README says so plainly.
  • 8149c4ce05 Show the connection dialog on play when the stream is down The "Off the Air" dialog used to be fired by the 20s status poll, so it threw itself over the page unprompted whenever nobody was broadcasting -- which is most of the time. It now opens only in response to someone actually pressing play, or a retry that failed. The poll still swaps the off-air artwork quietly, so the player reads as off air without a dialog in the way. This retires REVIEW_MODE: it existed to suppress that unprompted dialog, and there's nothing left to suppress. No temporary flags remain in the source. The play interceptor is bound on document in the CAPTURE phase, and that matters: binding on the button itself would not work, because at the target listeners run in registration order regardless of the capture flag and Amplitude registers first. Capturing on an ancestor lets stopPropagation keep the event from reaching the button at all, so Amplitude never starts a doomed playback attempt against a dead mount. When the stream is up the handler returns early and Amplitude gets the click untouched. Three bugs fixed on the way through: - Duplicate id="radio": the player's tower icon and the dialog's header icon both had it, so $('#radio') only ever matched the player and the dialog's icon was never being updated by the code that thought it was. The dialog's copy is static off-air artwork, so it just loses the id. - Stacked retry handlers: the click handler was bound inside showOffAir(), which the poll called every 20 seconds. Five minutes off air meant ~15 handlers firing on one click. Bound once at ready now. - Retry never retried: it animated the bar to 100% and announced ERROR from the `done` callback without asking the server anything. It now re-checks /api/status for real -- back on air closes the dialog, still down shows ERROR. Verified in a browser both ways: off air, play opens the dialog and Amplitude stays paused, retry re-checks and reports ERROR, and one click triggers exactly one progressbar call. With a live status, play goes straight through to Amplitude with no dialog, and a retry that succeeds closes the dialog itself.
  • 2136809609 Replace Bootstrap blue with the page slate; schedule is one line per day Every blue accent on the page now uses #484c57, the same colour as the page background, header and footer: - Player tab icons (fire/cd/user/calendar/time) were inheriting Bootstrap's link blue. The active tab keeps its #555 -- Bootstrap's own `.nav-tabs > li.active > a` is more specific than the new rule, so the selected tab still reads as selected instead of going flat. - Volume slider track, across every vendor prefix (webkit runnable-track, moz range-track, ms fill-lower/fill-upper) and their :focus shades. Scoped to the input[type=range] block so .progress-bar keeps its own colour. - Post button was already done. Verified: no Bootstrap blue (#337ab7 and its focus/active shades) remains on any visible element. Left white on purpose: the footer-nav icons sit on a rgba(0,0,0,.5) panel and the Post bullhorn sits on the now-slate button. Those were never blue, and recolouring them to the background would have made them illegible or invisible. Schedule: seven lines, one per day, each "Daveo Radio 24/7". Replaces ten placeholder entries for made-up shows on 2016 dates.
  • 6ad0da38a0 Drop the chat tab bar, give its space to the messages With one room it was a tab strip containing a single tab. Removing it frees 42px, which goes to #room_messages (220 -> 262) rather than just shortening the panel -- that keeps the chat level with the player beside it (420 vs 421) and gives the messages the extra room. getCurrentRoom() had a hidden dependency on that markup: it read the room name out of the active tab's text. With the tab gone it would have returned '', which the server rejects as an invalid room name, so posting would have failed silently -- no error, messages just vanishing. There's one room, so chat.js now names it directly via a ROOM constant, which also replaces the 'Lobby' strings that were already scattered through the file. That constant has to agree with the server's $MAIN_ROOM and the id of the pane in index.html, since messages are routed into '#<room> #room_messages'. Noted at both ends. The player's own tab strip (Recent/Tracks/Albums/...) is untouched -- only the chat's room tabs are gone, and the tooltip() call still belongs to the player. Verified in a real browser through Apache: a message typed into the box relays to the server as room "Lobby" and renders back into the pane; no console errors.
  • df5e494411 Single room, opt-in nickname, no password; proxy /radio via Apache Entry flow, permanently (not review-mode toggles): - Terms modal deleted, markup and handler. - No password anywhere. The field, the /other route and $ROOM_PASSWORD are gone. It was never real protection, so nothing was lost -- but the pretense is gone with it, which leaves the missing-auth gap visible rather than papered over. - Nickname is opt-in: a person-icon button sits left of the message box and opens the nickname dialog via data-toggle="modal". The dialog is dismissible now; unnamed users chat as 'anonymous'. Nothing is demanded on load. One room, and enforced: - Join/leave dropdown and #modal_joinroom removed, along with the now-dead room plumbing in chat.js (addRoom/addRoomTab/roomExists/getRoomName and the subscribe debug block). - The server no longer has subscribe/unsubscribe/getRooms handlers at all. That's the point -- hiding the UI alone would still let anyone emit 'subscribe' by hand and wander into a private room. Every socket joins mainRoom on connect and never leaves, so the socket.rooms check in 'newMessage' can only ever pass for that one room. Verified against the server: a hand-crafted subscribe is rejected, a message to an unjoined room reaches nobody, unsubscribe can't strand you out of the Lobby, and Lobby chat still relays with nicknames. Apache proxy (production is Apache, not nginx -- the daw vhost already proxies /ask to another node app "mirrors prod"; the README's nginx snippet was wrong and is replaced): ProxyPass /radio http://127.0.0.1:3000/radio upgrade=websocket ProxyPassReverse /radio http://127.0.0.1:3000/radio upgrade=websocket needs httpd 2.4.47+ (2.4.63 here), where mod_proxy_http tunnels upgrades itself -- mod_proxy_wstunnel is not required. Verified with a websocket-only client through Apache; polling fallback also works. Without it socket.io would silently degrade to long-polling. Styling: page background #484c57 (matching header, footer and the main site's body), the Post button in that same slate rather than Bootstrap blue, and the Daveo Radio banner gets margin above it -- margin, not padding, since a background image paints across the padding box.
  • View comparison for these 8 commits »

1 month ago

david pushed to main at david/dw

  • 77ecbf7b5a Fix og:image to an absolute shared URL; add og-image.jpg at the root og:image was relative (img/og_image.jpg) which scrapers can't resolve. Point it at the shared https://davidwindham.com/og-image.jpg and ship the image at the web root.

1 month ago

david pushed to main at david/daw

  • 4dc896daaa Add inc/seo.php: favicon, OpenGraph, Twitter, Person/WebSite JSON-LD No SEO plugin, so the theme emits social/schema metadata. Hooked to wp_head for pages/archives/front; skips single posts so dw_opengraph() in tweaks.php keeps ownership of post OG incl. the featured-video setup (og:video / twitter:player -> single-container). - Favicon links -> shared root-hosted /favicon.* (fixes WP pages inheriting Lychee's icon) - OG/Twitter with absolute URLs via home_url()/get_permalink() (survive the domain move) - Person + WebSite JSON-LD on the canonical https://davidwindham.com (https schema.org) - front-page.php (skips wp_head) calls dw_seo_head(); dynamic canonical; http->https itemtypes; removed its stale duplicate JSON-LD

1 month ago

david pushed to main at david/dw

  • ce7596e8db Footer: use the shared <daw-footer> web component; move References above it Replace the bespoke footer with the shared /embed/chrome.js <daw-footer> so /work matches the primary site and /rtc (brand, address, links, social). The page-specific References footnotes move to a section just above the footer.

1 month ago

david pushed to main at david/daw

  • 1460d98fb5 Fix: comment out a guard that leaked into a disabled function The bulk file_get_html guard pass (a253e83) matched a commented-out file_get_html($url) in the disabled mastodon() block and inserted a live `if (!$html) return;` inside it. Comment it out to match its block.

1 month ago

david pushed to main at david/daw

  • 7fbdf69e56 Footer: "David A. Windham" + zip, drop "USA" Match the header brand and add the postal code; the address block is now "102 Glenridge Circle / Greenwood, South Carolina 29646".

1 month ago

david pushed to main at david/daw

  • a253e835ab Guard external TIL/bookmarks fetches against timeouts inc/utils.php pulls TIL/bookmarks from davidawindham.com via simple_html_dom; when the fetch times out file_get_html() returns false and ->find() fataled the page (white screen on desk/archive). Added `if (!$html) return;` after all 20 direct file_get_html($url) calls so a slow/unavailable source degrades gracefully instead of crashing.

1 month ago

david pushed tag classic-final to david/daw

1 month ago

david pushed to main at david/daw

  • f6cf6183c6 Guest book, contact/archive page formats, front-page zoom + About link - Guest book: publish Gravity Forms "Guest" (form 2) active entries below the form on /contact/guests as a striped WP table (name/location/website/message, no email). Registered early in functions.php because the block-theme content renders before wp_enqueue_scripts (render_block on core/post-content). Styles in css/form.css. - Contact page: dark body + white "Contact" title + #dadada card (info left, form right), matching the other pages; dropped the body.contact light-bg override. - Archive page: same page format — leader width, white title, #dadada card, transparent legacy article shell. - Front page: zoom transition now fires on the Zeken Woozer and David A. Windham cards; David A. Windham links to /about; removed the bottom About link. - Desk: emoji on the Scroll/Archive buttons. - Rebuilt v4-style.min.css + v4-front.min.js.
  • 871b33e57f Bypass /photo in the BrowserSync proxy Browsing daw.stu:81/photo looped to daw.stu:81//daw.stu:81/photo/photo/. The proxy rewrites absolute URLs to protocol-relative so navigation stays on :81, turning Lychee's <base href="https://daw.stu/photo"> into "//daw.stu:81/photo". vue-router derives its base from <base href> and strips the origin with /^\w+:\/\/[^\/]+/ -- that needs a scheme, so the protocol-relative form slips through and becomes the router base, doubling every route. Nothing in the watch list covers /photo (separate app, own Vite build), so 302 it to the real vhost instead. WP pages still proxy with the bs snippet. Verified on a scratch port: /photo, /photo/ and /photo/gallery all 302 to daw.stu; / still 200s with browser-sync-client injected.
  • c11e216ea6 Rename footer-home -> footer-full and use it on all pages - parts/footer-home.html -> parts/footer-full.html; footer-home.php -> footer-full.php (block_template_part('footer-full')); theme.json templatePart footer-home -> footer-full - footer.php now renders footer-full, so the default get_footer() gives the full footer - Classic templates: get_footer('home') -> get_footer('full') (8 files) - Block templates: footer-home/footer part -> footer-full (single, search, index, 404, page, archive) — the previously-minimal-footer pages now show the full footer too - Home (front-page.php) keeps its bespoke footer; old parts/footer.html left orphaned
  • a0fa1d197d Music page overhaul, footer Music link/icon, relative cross-property links Music page (page-music.php, js/music.js, style.css): - Add page title + #dadada content card, consistent with desk/studio - Remove the analytics chart section (timeline + doughnuts + stats/legend) - Remove the AmplitudeJS player; add a "Tune In" / "Daveo Radio" button to /radio - Trim last.fm sections to top 20 (albums stay 200); give the 6-month and 1-year Artists/Tracks their own containers so they no longer double up - Drop the album play-count; render albums 5-per-row at full card width - Strip the now-dead AmplitudeJS + Chart.js libraries (js/music.js 124KB -> 24KB) Footer (parts/footer-home.html, icons.svg, bin/custom-icons/music.svg): - Rename "Audio" -> "Music" with a new dw-music note icon - Alphabetize the three footer link columns Cross-property links (parts/footer-home.html, page-contact.php): - Relativize /rtc and other in-domain links for the shared-chrome consolidation
  • 3077e0b6b2 home: zoom-into-card page transition + loader on desk/about; front-page icon swaps - Home nav cards (About/Desk/Studio/Work) now zoom the white card up until it fills the screen, fade the inner content out, then fade to a dark #222 veil so the handoff into the destination's dark loader is seamless, then navigate. (front-page.php CSS/veil, js/front-page.js click handler -> v4-front.min.js.) - Front-page animated icon: About morphs to the bubble, Work morphs to the buildings; removed the Contact nav link. - Page loader (loader.php) added to Desk (inline, like Studio) and About (hooked to wp_body_open, which block-rendered pages fire via core). - Fix: desk.js was fadeOut(100)-ing #loader on load, so the Desk loader flashed away instantly — removed it so loader.php's slow fade runs (matching Studio).
  • View comparison for these 93 commits »

1 month ago

david pushed to fse-migration at david/daw

  • f6cf6183c6 Guest book, contact/archive page formats, front-page zoom + About link - Guest book: publish Gravity Forms "Guest" (form 2) active entries below the form on /contact/guests as a striped WP table (name/location/website/message, no email). Registered early in functions.php because the block-theme content renders before wp_enqueue_scripts (render_block on core/post-content). Styles in css/form.css. - Contact page: dark body + white "Contact" title + #dadada card (info left, form right), matching the other pages; dropped the body.contact light-bg override. - Archive page: same page format — leader width, white title, #dadada card, transparent legacy article shell. - Front page: zoom transition now fires on the Zeken Woozer and David A. Windham cards; David A. Windham links to /about; removed the bottom About link. - Desk: emoji on the Scroll/Archive buttons. - Rebuilt v4-style.min.css + v4-front.min.js.
  • 871b33e57f Bypass /photo in the BrowserSync proxy Browsing daw.stu:81/photo looped to daw.stu:81//daw.stu:81/photo/photo/. The proxy rewrites absolute URLs to protocol-relative so navigation stays on :81, turning Lychee's <base href="https://daw.stu/photo"> into "//daw.stu:81/photo". vue-router derives its base from <base href> and strips the origin with /^\w+:\/\/[^\/]+/ -- that needs a scheme, so the protocol-relative form slips through and becomes the router base, doubling every route. Nothing in the watch list covers /photo (separate app, own Vite build), so 302 it to the real vhost instead. WP pages still proxy with the bs snippet. Verified on a scratch port: /photo, /photo/ and /photo/gallery all 302 to daw.stu; / still 200s with browser-sync-client injected.
  • View comparison for these 2 commits »

1 month ago

david pushed to fse-migration at david/daw

  • c11e216ea6 Rename footer-home -> footer-full and use it on all pages - parts/footer-home.html -> parts/footer-full.html; footer-home.php -> footer-full.php (block_template_part('footer-full')); theme.json templatePart footer-home -> footer-full - footer.php now renders footer-full, so the default get_footer() gives the full footer - Classic templates: get_footer('home') -> get_footer('full') (8 files) - Block templates: footer-home/footer part -> footer-full (single, search, index, 404, page, archive) — the previously-minimal-footer pages now show the full footer too - Home (front-page.php) keeps its bespoke footer; old parts/footer.html left orphaned

1 month ago

david pushed to fse-migration at david/daw

  • a0fa1d197d Music page overhaul, footer Music link/icon, relative cross-property links Music page (page-music.php, js/music.js, style.css): - Add page title + #dadada content card, consistent with desk/studio - Remove the analytics chart section (timeline + doughnuts + stats/legend) - Remove the AmplitudeJS player; add a "Tune In" / "Daveo Radio" button to /radio - Trim last.fm sections to top 20 (albums stay 200); give the 6-month and 1-year Artists/Tracks their own containers so they no longer double up - Drop the album play-count; render albums 5-per-row at full card width - Strip the now-dead AmplitudeJS + Chart.js libraries (js/music.js 124KB -> 24KB) Footer (parts/footer-home.html, icons.svg, bin/custom-icons/music.svg): - Rename "Audio" -> "Music" with a new dw-music note icon - Alphabetize the three footer link columns Cross-property links (parts/footer-home.html, page-contact.php): - Relativize /rtc and other in-domain links for the shared-chrome consolidation

1 month ago

david pushed to main at david/dw

  • 0706d19562 add page loader (mirrors the WordPress theme's loader.php) Self-contained loader block (dark #222 overlay + DW logo animation + fade-out on load) at the top of the body, matching the main site's loader.

1 month ago

david pushed to fse-migration at david/daw

  • 3077e0b6b2 home: zoom-into-card page transition + loader on desk/about; front-page icon swaps - Home nav cards (About/Desk/Studio/Work) now zoom the white card up until it fills the screen, fade the inner content out, then fade to a dark #222 veil so the handoff into the destination's dark loader is seamless, then navigate. (front-page.php CSS/veil, js/front-page.js click handler -> v4-front.min.js.) - Front-page animated icon: About morphs to the bubble, Work morphs to the buildings; removed the Contact nav link. - Page loader (loader.php) added to Desk (inline, like Studio) and About (hooked to wp_body_open, which block-rendered pages fire via core). - Fix: desk.js was fadeOut(100)-ing #loader on load, so the Desk loader flashed away instantly — removed it so loader.php's slow fade runs (matching Studio).

1 month ago

david pushed to main at david/til

1 month ago

david pushed to main at david/til

1 month ago

david pushed to fse-migration at david/daw

  • 2739d2f58f studio/projects/desk refinements + footer polish Studio (page-studio.php, js/studio.js, css/styles.css, style.css): - Move the three fact intro slides (web sites / stars / brain) to the end of the carousel; Studio leads. Reorder DOM, studio.js CONFIG, tab data-slide, and the active class together. - Halve the hero height: #caro min-height 400->200 so #studio-caro's 40vh binds. - Drop the Tools third column from every tab pane; copy is full-width with a 1.3rem serif at line-height 1.6 and a break between each italic quote and the body copy. - Tighten the tab line spacing; add a light Projects button under the tabs linking to /studio/projects/. - Subnav column: bottom-only radius so its square top stays snug under the slider; wrap the below-slider section in a dadada card (square top, rounded bottom) with left padding so the card shows around the subnav. Projects (page-projects.php, style.css): - Match the other pages: dadada card (1280) on the dark-blue body, white heading, and a little padding beneath each featured image. Desk (page-desk.php): - New Recommended Reading section (pulls the 'recommended' category) above Bookmarks. - Halve the bookmarks (7); bump Posts and Today I Learned to 21 each. Footer (parts/footer-home.html, style.css): - Drop the Spotify social icon; make the icons smaller (19px) and closer (5px gap) like the /work site. - Float the three link columns to the right above the social row, content-width with a fixed gap and tighter line spacing. Rebuilt v4-style.min.css.

1 month ago

david pushed to main at david/dw

  • db727b7bc1 nav: magic-line underline, sub-nav Playfair + underline, drop Now item - Top nav: floating magic-line underline that slides between items (mirrors the main site), resting under the current-page item. - Sub nav (Who/What/When/Why/How): Playfair Var at 1.35rem to match the top nav, with a left-to-right slide-in underline on hover. - Dropped the redundant Now item now that Now -> Desk on the main site.

1 month ago

david pushed to fse-migration at david/daw

  • 3d4ae04317 desk: two-column layout + search field/button height fix - Top section is now three equal columns: Now spans the first two (col-lg-8) with Posts + Today I Learned beneath it (col-md-6 each), and the meta sidebar (Edited / Bookmarks / Year) as the third (col-lg-4, no offset so the edges are symmetric). Removed the left divider on the sidebar. - Dropped the Projects, Topics and Tags sections; halved the Posts and TIL pulls (35 -> 18) so the left column height roughly matches the sidebar. - Search actions moved to the left with more top spacing. - Fixed the search submit button towering over the field: the field inherited a 20px bottom margin from .entry-content input which stretched the input-group (and the button) to 58px — zeroed it so both are 38px; button flex-centers its icon. (pull_til_tags()/dw_til_tags_html() added for a TIL tag cloud, currently unused after the Tags section was dropped.) - Rebuilt v4-style.min.css.
  • 3f6b9dedec desk: merge Now page into Desk; layout, nav & search overhaul - Merge the Now page content into the Desk template; drop page-now.php and 301-redirect /now/ -> /desk/ (inc/tweaks.php). Desk now leads with Doing + Projects, then a WP-side index row (Recent Posts / TIL / Recently Edited / Bookmarks / Year / Topics / Tags) and the pulled Docs/Notes/Lists/Learning/ Commiting/Listening/Watching/Reading/Playing sections. - Content card moved to body.desk .entry-content (was .row.desk / body.now); removed the now-only rules, the dead .row.desk card and h1#now. - Section headers unified to <h4> bold, no underlines. Year/Topics/Tags render inline and at the page font size (dropped the ul.tags 14px override). - Desk actions row: search inline with The Scroll (was Feed) + Archive; submit is now a magnifying-glass icon (new bin/custom-icons/magnifying-glass.svg). - TIL pulls read the local til/index.html when present (skips an HTTP round-trip per render); til()/pull_til()/bookmarks_pull() gained count params. - Offcanvas nav: floating magic-line underline; dropped the redundant Now item (also from the front-page card row). - Rebuilt v4-style.min.css + icons.svg.
  • 572c21659f footer: point Facebook link to personal profile facebook.com/davidawindham (page) -> facebook.com/windhamdavid (profile).
  • View comparison for these 3 commits »

1 month ago

david pushed to main at david/gwp

  • 1662dda49b Guard null my-account node in Howdy replacement get_node('my-account') can return null in some admin-bar render contexts; reading ->title on it emitted a warning on PHP 8.3 (silent on the server's 7.4). Bail out when the node is absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

1 month ago

david pushed to main at david/gwp

1 month ago

david pushed to main at david/dw

  • 35ae5e837b work nav: thin top bar + click-through fix - #worknav offcanvas collapses to a 68px bar (bottom:auto releases the base .offcanvas bottom:0 so height:auto no longer stretches full-height) - z-index 990 keeps the side-nav 'Who' link painted above where they overlap - pointer-events:none on the full-width .header-small bar (auto on its links/hamburger) so both navs stay clickable

1 month ago

david pushed to main at david/dw

  • 3a2c8c5596 top nav: logo scrolls to top, hamburger opens both navs, no backdrop, match font - Brand/logo reverted to href=#header (scroll to top), no longer opens the nav. - hamburger() now toggles BOTH the side menu and the #worknav top nav. - #worknav: data-bs-backdrop=false + data-bs-scroll=true so it sits at the top without covering/dimming the page. - Top-nav font -> variable Playfair (opsz 20), matching the main site's nav instead of the heavier static Playfair Display display cut.

1 month ago