# BW Dev — Roadmap

**Last Updated:** 2026-05-11
**Current Version:** 0.1.0

This roadmap spans multiple development sessions. Each phase is small enough to complete in 0.5–2 sessions. Phases must be done in order; each builds on the previous. See `docs/SPEC.md` for what each module must do, and `docs/ARCHITECTURE.md` for class structure.

When starting a session: pick the lowest-numbered phase whose checkbox is still empty. Update `docs/SESSION-LOG.md` with what got done. Update this file's checkboxes only when a phase is fully verified on the dev site.

## Phase 0 — Scaffold ☑

- ☑ Run `tools/new-plugin.sh bw-dev "BW Dev" "..."`
- ☑ Preserve original brief at `docs/notes-from-adi.txt`
- ☑ Write SPEC.md, ARCHITECTURE.md, ROADMAP.md, SESSION-LOG.md, HANDOFF-NOTES.md, CLAUDE.md
- ☑ Plugin activated on dev site (https://bw-plugins.demoing.info)

## Phase 1 — Settings framework ☑

Goal: tabbed `Settings → BW Dev` page exists, module enable/disable toggles work, and at least one trivial setting persists. No real modules yet.

- ☑ `includes/interface-bw-dev-module.php` — module contract
- ☑ `includes/class-bw-dev-settings.php` — root option, dispatching sanitize
- ☑ `includes/class-bw-dev-plugin.php` — registry + lifecycle
- ☑ `includes/class-bw-dev-admin-page.php` — tabbed renderer
- ☑ Stub `BW_Dev_Module_Demo` to exercise the lifecycle (still in place — remove as first step of Phase 2)
- ☑ Verified via WP-CLI: toggling demo off hides its tab; saving a message renders an admin_notices entry on every admin page; XSS sanitization works
- ☑ `tools/cleanup-scan.sh bw-dev` — clean
- ☑ `tools/security-scan.sh bw-dev` — clean (after refactoring two `$_GET` reads through `wp_unslash($_GET)` + sanitize_key)
- ☑ Lint via `php -l` in the WP container — all files OK
- ☐ Visual verification by adi at https://bw-plugins.demoing.info/wp-admin/options-general.php?page=bw-dev (recommended before Phase 2)
- ☐ Remove demo module at start of Phase 2

## Phase 2 — Favicon module ☑

Simplest module — the warmup port.

- ☑ `class-bw-dev-module-favicon.php` ported from `bw-favicon`
- ☑ Settings tab: media picker + URL field + clear button + live preview, with `sanitize_callback` (via module's `sanitize()`) the original was missing
- ☑ `wp_head` / `admin_head` / `login_head` injection at priority 9999 — verified via `$wp_filter` introspection
- ☑ `wp_enqueue_media()` only when the Favicon tab is active (not the whole BW Dev page)
- ☑ Sanitize rejects `javascript:` and unsafe protocols (esc_url_raw strips them to empty)
- ☑ Demo module removed (file, require_once, build_modules() entry)
- ☑ Cleanup-scan, security-scan, php-lint all clean
- ☑ CHANGELOG.md updated

## Phase 3 — Sticky Elements module ☑

- ☑ `class-bw-dev-module-sticky.php` ported from `bw-sticky-settings` (~270 LOC)
- ☑ Assets moved to `assets/js/sticky.js`, `assets/js/sticky-admin.js`, `assets/css/sticky-admin.css` with `bw-dev-*` prefix throughout
- ☑ Multi-element editor UI in settings tab (collapsible rows, Add/Remove, Edit toggle, live title from selector input)
- ☑ Frontend script only enqueues when ≥1 element is enabled
- ☑ Sanitize clamps mobile_breakpoint to 320–1200, z_index ≥ 1, skips empty-selector rows, re-indexes array
- ☑ Identifier rename documented in KNOWN-ISSUES (CSS class names + window.bwStickyConfig)
- ☑ Cleanup-scan, security-scan, php-lint all clean
- ☑ CHANGELOG.md updated

## Phase 4 — Admin Columns module ☑

- ☑ `class-bw-dev-module-admin-columns.php` consolidates the source's 4-class split into one module class (~500 LOC + ~100 LOC of JS/CSS)
- ☑ AJAX handlers audited — source had nonce + capability checks; preserved them
- ☑ AJAX action names re-prefixed: `bw_dev_set_featured_image`, `bw_dev_scan_meta_keys`
- ☑ Nonces re-prefixed: per-post `bw_dev_set_featured_image_<id>` + global `bw_dev_admin_columns_nonce`
- ☑ Assets in `assets/css/admin-columns-{settings,list}.css` and `assets/js/admin-columns-{settings,list}.js`
- ☑ Settings tab uses a sub-tab nav per post type (`&ptype=<slug>` query param). Hidden fields preserve other sub-tabs' data.
- ☑ Column IDs re-prefixed: `bw_dev_featured_image`, `bw_dev_tax_*`, `bw_dev_meta_*`
- ☑ CSS classes re-prefixed throughout
- ☑ Cleanup-scan, security-scan, php-lint all clean
- ☑ CHANGELOG.md updated

## Phase 5 — YouTube Block module ☑

- ☑ `class-bw-dev-module-youtube.php` ported from `bw-youtube-embed`
- ☑ Block bundle at `blocks/youtube/` with renamed block: `bw-dev/youtube`
- ☑ ACF field name configurable per-block via InspectorControls sidebar (NEW). Empty = use global default. Global default exposed to editor via `window.bwDevYouTube.defaultField` for placeholder hint.
- ☑ Shortcode `[bw_dev_youtube]` always registered; backwards-compat alias `[bw_youtube]` registered only when the legacy `bw-youtube-embed` plugin is inactive (prevents conflict during side-by-side migration; detected via `function_exists( 'bw_youtube_shortcode_handler' )`)
- ☑ Settings tab: global default ACF field name + supported URL formats + shortcode docs
- ☑ Verified on dev site: video-ID extraction (5 URL shapes + query-string variant + invalid + XSS-style payload), per-block override, global fallback, legacy alias renders identically, block + shortcode registration, sanitize edge cases
- ☑ Video-ID regex tightened: `[A-Za-z0-9_-]+` (was `[^?&"'>\s]+`) — restricts to YouTube's actual ID charset
- ☑ Identifier rename documented in KNOWN-ISSUES (block name + CSS classes; shortcode aliased)
- ☑ Cleanup-scan, security-scan, php-lint all clean
- ☑ CHANGELOG.md updated

## Phase 6 — Post Link Block module ☑

- ☑ `class-bw-dev-module-post-link.php` ported from `bw-pretty-post-link`
- ☑ Two blocks renamed: `bw-dev/post-link-list` (parent), `bw-dev/post-link-item` (child)
- ☑ REST endpoint renamed: `bw-dev/v1/post-types` (gated by `edit_posts`)
- ☑ Editor + frontend assets bundled in `blocks/post-link/`
- ☑ CSS classes renamed `.bw-ppl*` → `.bw-dev-post-link*`
- ☑ Empty catch in editor.js made non-empty with explanatory comment (cleanup-scan compliance)
- ☑ Cleanup-scan, security-scan, php-lint all clean
- ☑ CHANGELOG.md updated

## Phase 7 — White-label layer ☑

- ☑ `includes/class-bw-dev-brand.php` — resolver + inline JS exposure + plugin-list filter + block category registration
- ☑ Branding settings tab: plugin display name, block category label, block title prefix (all three persisted under `bw_dev_settings.brand`)
- ☑ `assets/js/branding.js` filters `blocks.registerBlockType` for any `bw-dev/*` block — strips the default `BW ` prefix and applies the configured prefix; reassigns blocks to the `bw-dev-blocks` category (stable slug)
- ☑ Settings → BW Dev submenu label uses resolved brand (via `BW_Dev_Admin_Page::add_menu()`)
- ☑ Plugin list display name rewritten via `all_plugins` filter
- ☑ Block category registered with brand label via `block_categories_all` filter
- ☑ Verified rebrand: set brand to "Acme Tools" / "Acme Blocks" / "Acme " — plugin list, submenu, and block category all reflect; resetting to empty returns defaults
- ☑ CHANGELOG.md updated

## Phase 8 — Activation migration ☑

- ☑ `includes/class-bw-dev-migration.php` — `BW_Dev_Migration::on_activation()` wired via `register_activation_hook`
- ☑ Reads legacy options and writes the new schema:
  - `bw_favicon_url` → `bw_dev_settings[favicon][url]`
  - `bw_sticky_settings` → `bw_dev_settings[sticky][elements]`
  - `bw_youtube_embed_settings` → `bw_dev_settings[youtube][acf_field]`
  - `bw_admin_column_settings` → `bw_dev_settings[admin_columns]` (same shape)
- ☑ Conservative merge: only fills in keys NOT already present in `bw_dev_settings`
- ☑ Idempotent — tracked via `bw_dev_migration_version` option; re-activation is a no-op
- ☑ Legacy options NOT deleted — user manually deactivates source plugins after verifying
- ☑ `uninstall.php` updated to delete `bw_dev_migration_version` too
- ☑ Verified on dev site: seed legacy options → activate → schema populated → change legacy option → re-activate → no-op confirmed

## Phase 9 — Polish + 1.0.0 release (pending rian)

- ☑ `tools/cleanup-scan.sh bw-dev` — clean
- ☑ `tools/security-scan.sh bw-dev` — clean
- ☑ `.pot` translation template generated at `languages/bw-dev.pot` (620 lines, via `wp i18n make-pot`)
- ☑ README.md filled in with real feature list (replaced the scaffold placeholders)
- ☐ `tools/test-plugin.sh bw-dev` — requires docker access (rian only)
- ☐ Bump to 1.0.0 via `tools/bump-version.sh bw-dev 1.0.0` (adi can run this — bump-version.sh doesn't require rian)
- ☐ Release via `tools/release.sh bw-dev 1.0.0` — **must be run as `rian`** (release.sh has `require_rian` and runs `test-plugin.sh` which needs docker)
- ☐ Add a product page for bw-dev on `plugins.bowden.works`

## Phase 11 — Admin Note module ☑ (added post-plan by request)

Not in the original 5-plugin combine. Sourced from `/srv/apps/regent/wp-content/plugins/bw-admin-note/` (the standalone plugin running on regent + promobix today; the `bw-plugins/wp-content/plugins/bw-admin-note/` directory is an empty placeholder).

- ☑ `class-bw-dev-module-admin-note.php` — registers `_bw_admin_note` post meta for every public REST post type (caught both at `init` and via `registered_post_type` for late-registered CPTs).
- ☑ `assets/js/admin-note-editor.js` — `PluginDocumentSettingPanel` sidebar + `editor.BlockListBlock` HOC that injects a yellow banner above the first block when a note exists.
- ☑ `assets/css/admin-note.css` — banner fade-in, sidebar textarea, settings page layout.
- ☑ **Compat decision**: post-meta key stays `_bw_admin_note` (not re-prefixed to `_bw_dev_admin_note`) so sites switching from the standalone plugin keep their existing notes. Documented in module's class comment + settings tab UI.
- ☑ Settings tab: per-post-type enable checkboxes + "How it works" info box + Notes Index table (direct SQL query against `wp_postmeta`).
- ☑ Migration in `BW_Dev_Migration` reads `bw_admin_note_settings` → writes `bw_dev_settings.admin_note`. Idempotent.
- ☑ `BW_Dev_Migration::legacy_map()` extended with `bw-admin-note/bw-admin-note.php` → `admin_note`.
- ☑ `uninstall()` intentionally leaves `_bw_admin_note` post meta intact — notes are editorial content; reinstall or switch-back recovers them.
- ☑ Verified: sanitize, meta registration on `post`/`page`, settings tab rendering with seeded note, notes index, migration round-trip, idempotency.
- ☑ Cleanup-scan, security-scan, php-lint all clean.

## Phase 10 — SVG Upload module ☑ (added post-plan by request)

Not in the original 5-plugin combine. Requested by adi after Phase 8 completed because every site needs the same Safe-SVG-style plugin installed separately.

- ☑ `class-bw-dev-module-svg-upload.php` — single module file, no external dependencies.
- ☑ Adds `image/svg+xml` to `upload_mimes` only if current user is in the allowed-uploaders group (default admin-only; settable to "anyone with upload_files capability").
- ☑ Normalizes `wp_check_filetype_and_ext` so libmagic edge cases don't bounce SVGs.
- ☑ Sanitizes on `wp_handle_upload_prefilter` — DOMDocument-based, no Composer dep.
- ☑ Strips: `<script>`, `<foreignObject>`, `<iframe>`, `<embed>`, `<object>`, `<animate*>`, `<set>`, `<handler>`, `<a>`; all `on*` attrs; `href`/`xlink:href` with `javascript:` or `data:`; `style` with `expression()` or `javascript:`; XML processing-instructions and external DTDs (XXE).
- ☑ Media library preview fix via `wp_prepare_attachment_for_js`.
- ☑ Verified: 8 attack-payload tests pass; legitimate SVG preserved; broken input rejected; capability gate works (admin sees `image/svg+xml` MIME, anon does not).
- ☑ `.svgz` round-trip works (gzdecode → sanitize → gzencode).
- ☑ Cleanup-scan, security-scan, php-lint all clean.

## Post-1.0 sprint ☑ (2026-05-15 / 2026-05-16)

The phased pre-1.0 roadmap above ended at the 1.0.0 release. After 1.0.1 patched the ABSPATH-guard scanner warnings, a two-day sprint added eleven new modules + a handful of patches, all pending rian's release pass. See `CHANGELOG.md` for per-version detail and `docs/SESSION-LOG.md` for session-by-session narrative.

**Six "Kadence-Pro-gap" modules** (1.1.0 → 1.6.0) — features Kadence Pro doesn't ship that every BW client build ends up rebuilding:
- ☑ `disable_comments` (1.1.0, editor_admin)
- ☑ `login_redirect` (1.2.0, security)
- ☑ `login_branding` (1.3.0, core)
- ☑ `admin_menu_role` (1.4.0, editor_admin)
- ☑ `dashboard` (1.5.0, editor_admin — extended in 1.9.2 with plugin-widget auto-detect)
- ☑ `maintenance_mode` (1.6.0, frontend)

**Five ad-hoc modules** (1.7.0 → 1.11.0):
- ☑ `scheduled_actions` (1.7.0, editor_admin — Post Expirator replacement; 1.7.1 added the schedule-index table)
- ☑ `sidebars` (1.8.0, frontend — register / unregister widget areas + shortcode)
- ☑ `server_info` (1.9.0, core — dev environment dump; 1.9.1 added themes + plugins listings; 1.9.2 was the dashboard-module extension above)
- ☑ `import_export` (1.10.0, core — settings JSON backup/restore — was on the original "Future" list)
- ☑ `site_knowledge` (1.11.0, indexing — `knowledge.json` AI-context export with SEO meta auto-detect from Yoast / Rank Math / SEOPress / AIOSEO; optional developer mode adds an environment block)

**One reverted experiment** (1.8.1 → 1.8.4):
- ☑ 1.8.1 added a "Purge all comments" admin-post.php action to the Disable Comments tab; 1.8.2 and 1.8.3 chased two integration bugs (nested-form HTML, then admin-post.php nopriv-routing on HTTPS sites). 1.8.4 reverted the entire purge feature — the admin-post.php path kept surfacing edge cases, and historical-comment cleanup via WP-CLI is acceptable. The clearer "how to disable" callout from 1.8.1 was kept.
- ☑ The same 1.8.2 + 1.8.3 fixes were applied to `login_log`'s purge button for consistency.

**Conventions established by the sprint** (apply to any future module):
- Prefer inline `admin_init` action dispatch over `admin-post.php` for actions scoped to a settings page.
- If admin-post.php is necessary, register on BOTH `admin_post_<action>` AND `admin_post_nopriv_<action>` — handler's own caps + nonce checks keep the nopriv hook safe.
- For destructive buttons inside a settings-tab form, use HTML5 `<button formaction>` rather than nesting a new `<form>` (HTML doesn't allow nested forms).
- Custom-named nonce field per module (`_bw_dev_<module>_<action>_nonce`) so it doesn't clobber the outer settings `_wpnonce`.
- `esc_attr()`-wrap any `onclick="..."` JS that interpolates a JSON-encoded string — `wp_json_encode()` produces raw `"` that collide with the attribute quotes.
- For modules with cron: register the schedule from BOTH `BW_Dev_Migration::on_activation` (reactivation path) AND `register()` on `init` (auto-update path). See `scheduled_actions::schedule_cron()`.
- When editing `bw-dev.php`'s require_once block AND `class-bw-dev-plugin.php`'s `build_modules()` in the same session: do bw-dev.php FIRST, await success, THEN the plugin class. Parallel edits hit a stale-read trap that produces transient "class not found" fatals.

**Module count: 20 (1.0.0) → 31 (1.11.0).**

## 1.12.0 — Image Optimizer ☑ (2026-05-27, released)

Three-phase Image Optimizer module (resize + JPEG compress on upload, watermark removal ported from `crop.bowden.works`, multi-profile tabbed UI with sticky live preview). Shipped as one minor. 1.12.1 followed: preview switched from auto-on-change to a button-triggered "Update preview" (Imagick too slow on Flywheel). See `CHANGELOG.md`.

## 1.13.0 — Merge three standalone plugins ☑ (2026-05-28, staged — not yet released)

Folded three standalone Bowden Works plugins into BW Dev as **Front-end** modules, one minor:

- **`inline_search`** ← `bw-inline-search` (expand-from-icon search, shortcode + widget + theme-toggle replacement).
- **`animate_row`** ← `bw-animate-row` (global AOS scroll animation; **AOS vendored** into `assets/vendor/aos/` instead of the unpkg CDN).
- **`scroll_down`** ← `bw-scroll-down` (scroll indicator, 6 icons, shortcode + trigger-class).

Migration bumped to v2 to pull each plugin's legacy options into `bw_dev_settings`. Scans clean; programmatic WP-CLI verification done; browser QA + `release.sh` pending. Source plugins left untouched as reference, deactivated per-site by adi once verified.

## Future (post-1.13.0)

- Per-module debug log toggle for troubleshooting.
- Optional: switch SVG sanitizer to `enshrined/svg-sanitize` (via Composer) for the strictest sites — covers more obscure SVG/SMIL constructs than the bundled DOMDocument approach.
- Optional: full `phpinfo()` dump in Server Info via iframe-embed (skipped in 1.9.0 because inline emit breaks the admin DOM).
- More modules — to be added when use cases arise. Don't pre-build.
- Re-evaluate whether Admin Columns deserves to spin out as its own plugin (largest module by far).
- Harden `tools/test-plugin.sh` ABSPATH check (false-positive class still exists; tooling-side, not plugin-side).
