# BW Dev — Claude Development Guide

**Version:** 1.20.0 (41 modules; 1.12.0 added Image Optimizer with watermark removal and multi-profile UI; 1.12.1 switched its preview to a button-triggered "Update preview" pattern after rian found Imagick on Flywheel too slow for live updates; 1.13.0 merged three standalone plugins as Front-end modules — Inline Search, Animate Row (AOS now vendored, not CDN-loaded), Scroll Down — and added a new Analytics module for GTM + Google Search Console verification; 1.13.1 simplified Analytics (removed the Emit-GTM toggle — emission now keyed off gtm_id alone) and tried to fix an Animate Row blank-hero bug; 1.13.2 fully disabled Animate Row after the 1.13.1 partial fix still left below-the-fold rows hidden — pending a proper rewrite, the module's enqueue is short-circuited site-wide while saved state is preserved; 1.14.0 added a Flywheel Application Passwords bypass module — companion to flywheel_auto_updates — that hooks `wp_is_application_passwords_available` at priority 99 to override Flywheel's hosting-level block; 1.15.0 added a BW Cron module — fleet-wide WordPress cron management with a dead-man's-switch that disables native WP-Cron only while a central trigger from mosiah is demonstrably alive, auto-reverts to native cron if the hub goes silent past the fallback window; 1.16.0 added a Request Firewall module — returns an instant 410 for unambiguous junk/exploit-probe URLs from a self-managed mu-plugin stub, before the expensive WP bootstrap, to stop junk-request floods from exhausting the PHP worker pool; 1.17.0 upgraded Request Firewall to v2 — autonomous load-shedding: a known-good allowlist (generated from the site's real pages) + pressure detection (shared memcached/APCu/flat-file counter store + shutdown 404-recorder) so that under abnormal load ANY non-real-page request is shed with an instant 410 before bootstrap, auto-engaging at a 404-rate threshold and auto-disengaging when the flood stops, zero human action, fail-open if the allowlist is missing/stale; **1.17.1–1.17.2 hardened it then 1.18.0 REMOVED it entirely** — the generated mu-plugin's per-request `flock()` on a file-backed store serialized PHP workers on Flywheel's networked filesystem and caused a production outage (aismartventures.com, 2026-06-11); 1.18.0 makes the module inert and auto-decommissions the stub on every site. The host's real problem is resource/stack + no object cache, not 404 floods — do not re-ship in-PHP load-shedding; 1.19.0 added a Launch Checklist module — an admin-only, data-driven site-launch/optimization wizard (declarative sections/items registry; auto steps self-verify, manual steps are checkboxes; v1 ships a Performance & Caching section) built to the post-firewall hard constraints: admin-only, zero front-end hooks, zero hot-path/filesystem work; 1.20.0 added an External Links module — opt-in, **default-OFF** per site (via the `bw_dev_module_default_enabled` filter in `boot()`); a tiny client-side script sets `target="_blank"` + `rel="noopener"` on off-site links after load — comprehensive across page builders and cache-friendly, zero per-request PHP) | **Last Updated:** 2026-07-20

## What this plugin is

The single Bowden Works "dev toolkit" plugin used on every Kadence-based site. Combines 5 previously-separate plugins (admin columns, favicon, sticky elements, post-link blocks, YouTube embed) into one BW-compliant plugin with a unified settings page at `Settings → BW Dev` and a white-label layer for client-facing block names.

The original brief from adi is at `docs/notes-from-adi.txt`. Read it once.

## Before making changes

1. Read `docs/HANDOFF-NOTES.md` FIRST — it has the current state and 4 pickup scenarios (fresh Claude, rian releasing, per-site rollout, adding an 8th module). Start there to understand WHAT TO DO NEXT.
2. Read `docs/SESSION-LOG.md` (newest entry at top) for what was just done.
3. Read `docs/ROADMAP.md` for the phase history.
4. Read `docs/KNOWN-ISSUES.md` before recommending anything migration-related.
5. Read `docs/SPEC.md` / `docs/ARCHITECTURE.md` only if HANDOFF doesn't answer your question.

## Hard rules for this plugin

- **Settings page lives at `Settings → BW Dev`** — a submenu under WordPress's built-in Settings menu, NOT a top-level admin menu. (Adi's explicit preference: "not too front facing".)
- **Single root option `bw_dev_settings`** — all persisted state nested under this one option key. No proliferation of `bw_dev_<thing>_settings`.
- **Modules are individually toggleable** — disabled modules register zero hooks, zero assets, zero REST routes.
- **White-label scope = blocks + plugin admin name only.** Tab labels inside `Settings → BW Dev` stay BW-branded; clients don't visit there.
- **DO NOT modify the 5 source plugins** at `/srv/apps/bw-plugins/wp-content/plugins/bw-{admin-column,favicon,pretty-post-link,sticky-settings,youtube-embed}/`. They are the reference implementation until each module is verified ported. Adi will deactivate them manually once each is replaced.
- **Re-prefix everything** when porting: `bw_favicon_*` → `bw_dev_*`, `bw/youtube` → `bw-dev/youtube`, AJAX action names included.
- **Migration is one-way and non-destructive.** Activation reads legacy options into `bw_dev_settings` if it's empty. Never delete legacy options — the old plugin's uninstall hook handles its own cleanup.

## Module slug ↔ source plugin map

| Module slug (PHP) | UI label | Source plugin (DO NOT EDIT) |
|---|---|---|
| `admin_columns` | Admin Columns | `bw-admin-column` |
| `favicon` | Favicon | `bw-favicon` |
| `sticky` | Sticky Elements | `bw-sticky-settings` |
| `post_link` | Post Link Block | `bw-pretty-post-link` |
| `youtube` | YouTube Block | `bw-youtube-embed` |
| `svg_upload` | SVG Upload | *(none — net-new feature)* |
| `admin_note` | Admin Note | `bw-admin-note` (deployed on regent + promobix; the bw-plugins copy is empty) |
| `analytics` | Analytics | *(none — net-new feature; GTM + Search Console verification meta)* |
| `flywheel_app_passwords` | Flywheel Application Passwords | *(none — net-new; Flywheel-specific filter unblock at priority 99)* |
| `bw_cron` | BW Cron | *(none — net-new; fleet-wide cron management with dead-man's-switch. Hub spec at `docs/BW-CRON-HUB.md`)* |
| `request_firewall` | Request Firewall (removed) | *(REMOVED in 1.18.0 — caused a production outage. The generated mu-plugin did a blocking `flock()` on a file-backed store on every request, which serializes PHP workers on Flywheel's networked filesystem and exhausts the pool. The module is now inert and only runs a one-time, idempotent decommission that deletes the stub + data dir and disables itself on every site automatically. Do NOT re-add load-shedding — see `docs/REQUEST-FIREWALL.md` post-mortem and the hard gate on any future firewall code.)* |
| `launch_checklist` | Launch Checklist | *(none — net-new; admin-only, data-driven site-launch/optimization wizard. Declarative `sections()`/`items()` registry; auto steps self-verify (plugin-active, PHP/OPcache/Redis, on-demand homepage fetch), manual steps are checkboxes; state under `bw_dev_settings['launch_checklist']`, saved via AJAX. v1 = Performance & Caching section. Add sections as registry entries — no UI changes. Strictly admin-only / no front-end hooks / no hot-path work.)* |
| `external_links` | External Links | *(none — net-new; opt-in **default-OFF** front-end module. Client-side JS (`assets/js/external-links.js`) sets `target="_blank"` + `rel="noopener"` on off-site links after load — catches every link regardless of builder/theme, cache-friendly, zero per-request PHP. Options: extra internal domains, optional `nofollow`. Default-off via the `bw_dev_module_default_enabled` filter in `class-bw-dev-plugin.php` boot().)* |
| `inline_search` | Inline Search | `bw-inline-search` |
| `animate_row` | Animate Row | `bw-animate-row` (AOS vendored in `assets/vendor/aos/`, was unpkg CDN) |
| `scroll_down` | Scroll Down | `bw-scroll-down` |

Block names use hyphens (`bw-dev/post-link-list`), PHP slugs use underscores (`post_link`).

## Process

This plugin follows the standard BW Plugins process defined in the umbrella `/srv/apps/bw-plugins/CLAUDE.md` — versioning protocol (default = patch), release gating (never `release.sh` without explicit "release it" from rian), session protocol, and the Always / Never lists. Plus the full release flow in `/srv/apps/bw-plugins/docs/RELEASE-PROCESS.md`.

The hard rules below are plugin-specific overrides and additions to the umbrella, not duplicates of it.

## Session Protocol (plugin-specific addition)

Standard session protocol plus:

- At session start, also read `docs/ROADMAP.md` (this plugin tracks phases there).
- At session end, also tick any completed checkboxes in `docs/ROADMAP.md`.

## Architecture Quick Reference

- Main file: `bw-dev.php` (thin — header, constants, `plugins_loaded` bootstrap).
- Core classes: `includes/class-bw-dev-{plugin,settings,brand,admin-page}.php`.
- Module interface: `includes/interface-bw-dev-module.php`.
- Modules: `includes/modules/class-bw-dev-module-<slug>.php`.
- Admin views: `admin/views/` and `admin/tab-renderers/`.
- Blocks: `blocks/post-link/` and `blocks/youtube/` (each with `block.json`).
- Shared assets: `assets/{css,js,images}/`.
- Vendored: `vendor/plugin-update-checker/` (never edit).

## Plugin-specific Never (additions to the umbrella)

The umbrella covers vendor edits, secrets, scan bypass, etc. These are bw-dev-only:

- Edit the 5 source plugins under `/srv/apps/bw-plugins/wp-content/plugins/bw-{admin-column,favicon,pretty-post-link,sticky-settings,youtube-embed}/` — they're the reference implementation until each module is verified ported.
- Auto-deactivate or delete the source plugins from bw-dev's activation hook — adi deactivates them manually once each port is verified.
- Add a top-level admin menu — `Settings → BW Dev` only (adi's explicit preference).
