diff --git a/bd-bookshelf-plugin/bd-bookshelf.php b/bd-bookshelf-plugin/bd-bookshelf.php
deleted file mode 100644
index 49700da..0000000
--- a/bd-bookshelf-plugin/bd-bookshelf.php
+++ /dev/null
@@ -1,158 +0,0 @@
-init_hooks();
- }
-
- /**
- * Initialize hooks
- */
- private function init_hooks() {
- register_activation_hook(__FILE__, [$this, 'activate']);
- register_deactivation_hook(__FILE__, [$this, 'deactivate']);
-
- add_action('init', [$this, 'init']);
- add_action('plugins_loaded', [$this, 'plugins_loaded']);
-
- // Initialize components
- add_action('init', [$this, 'init_components']);
- }
-
- /**
- * Plugin activation
- */
- public function activate() {
- $database = new BDM_Database();
- $database->create_tables();
-
- // Flush rewrite rules
- flush_rewrite_rules();
-
- // Store activation time
- update_option('bd_bookshelf_activation_time', current_time('timestamp'));
- }
-
- /**
- * Plugin deactivation
- */
- public function deactivate() {
- // Clear scheduled events
- wp_clear_scheduled_hook('bd_bookshelf_sync_ratings');
-
- // Flush rewrite rules
- flush_rewrite_rules();
- }
-
- /**
- * Init hook
- */
- public function init() {
- // Load text domain
- load_plugin_textdomain('bd-bookshelf', false, dirname(BD_BOOKSHELF_BASENAME) . '/languages');
- }
-
- /**
- * Plugins loaded hook
- */
- public function plugins_loaded() {
- // Check for updates, etc.
- }
-
- /**
- * Initialize components
- */
- public function init_components() {
- // Initialize admin
- if (is_admin()) {
- new BDM_Admin();
- new BDM_Migration();
- }
-
- // Initialize public facing features
- new BDM_Shortcodes();
- new BDM_GDPR();
-
- // Initialize rating sync (cron)
- new BDM_Rating_Sync();
- }
-}
-
-/**
- * Initialize plugin
- */
-function bd_bookshelf() {
- return BD_Bookshelf::instance();
-}
-
-// Start the plugin
-bd_bookshelf();
diff --git a/bd-bookshelf-plugin/readme.txt b/bd-bookshelf-plugin/readme.txt
deleted file mode 100644
index 59551dc..0000000
--- a/bd-bookshelf-plugin/readme.txt
+++ /dev/null
@@ -1,77 +0,0 @@
-=== BD Bookshelf ===
-Contributors: bdmurphy
-Requires at least: 6.0
-Tested up to: 6.5
-Requires PHP: 7.4
-Stable tag: 0.1.0
-License: GPLv2 or later
-License URI: https://www.gnu.org/licenses/gpl-2.0.html
-
-Copyright 2026 B.D. Murphy
-
-Book management plugin for authors with ISBN tracking, retail links, and rating synchronization.
-
-== Description ==
-
-BD Bookshelf is a comprehensive book management plugin designed for authors. It provides:
-
-* **Book Management** - Custom database tables for efficient book storage
-* **ISBN Tracking** - Support for 4 ISBN formats (eBook, Paperback, Hard Cover, Audio)
-* **Retail Links** - Automatic link generation from ISBNs for Amazon, B&N, Apple Books, Google Play, and Kobo
-* **Rating Synchronization** - Automated syncing of ratings from Amazon and Goodreads via web scraping
-* **Shortcodes** - Display retail grids and ratings anywhere on your site
-* **Series Management** - Organize books into series with ordering
-* **Author Management** - Support for up to 3 authors per book
-* **SEO Metadata** - Custom meta tags and Open Graph data for books
-* **GDPR Compliance** - Cookie consent banner for affiliate tracking
-* **Click Analytics** - Track clicks on custom redirect shortcodes
-* **Data Migration** - Import existing books from WordPress posts or CSV files
-
-== Installation ==
-
-1. Upload the `bd-bookshelf` folder to the `/wp-content/plugins/` directory
-2. Activate the plugin through the 'Plugins' menu in WordPress
-3. Navigate to the 'Books' menu to start adding books
-4. Configure affiliate codes in Settings > Books > Settings
-
-== Frequently Asked Questions ==
-
-= How do I add a new book? =
-
-Go to Books > Add New Book. Fill in the title, description, ISBNs, and other details. Retail links are automatically generated when you enter ISBNs.
-
-= How do I display retail links? =
-
-Use the shortcodes `[sell_ebook]`, `[sell_pb]`, `[sell_hc]`, or `[sell_audio]` with the book ID:
-
-`[sell_ebook id="123"]`
-
-= How do I display ratings? =
-
-Use the `[rating]` shortcode:
-
-`[rating title="My Book" format="both"]`
-
-= How often are ratings synced? =
-
-Ratings are automatically synced every 12 hours. You can also trigger a manual sync from the Rating Sync page.
-
-== Changelog ==
-
-= 0.1.0 =
-* Initial release
-* Book management with custom database tables
-* ISBN tracking for 4 formats
-* Automatic retail link generation
-* Rating synchronization from Amazon and Goodreads
-* Shortcode suite for retail grids and ratings
-* Series and author management
-* SEO metadata
-* GDPR compliance
-* Click analytics
-* Data migration tools
-
-== Upgrade Notice ==
-
-= 0.1.0 =
-Initial release of BD Bookshelf plugin.
diff --git a/bd-bookshelf-plugin/uninstall.php b/bd-bookshelf-plugin/uninstall.php
deleted file mode 100644
index dacee1f..0000000
--- a/bd-bookshelf-plugin/uninstall.php
+++ /dev/null
@@ -1,30 +0,0 @@
-drop_tables();
-
-// Remove options
-global $wpdb;
-$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'bd_bookshelf_%'");
-
-// Remove transients
-$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_bd_bookshelf_%'");
-$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_bd_bookshelf_%'");
diff --git a/bd-bookshelfhelper/bd-bookshelfhelper.php b/bd-bookshelfhelper/bd-bookshelfhelper.php
deleted file mode 100644
index 71284bf..0000000
--- a/bd-bookshelfhelper/bd-bookshelfhelper.php
+++ /dev/null
@@ -1,61 +0,0 @@
- Add Book
-2. Fill in book details including ASIN/ISBN
-3. Add localized content for each supported language
-4. Set retailer purchase links
-5. Add reviews and ratings
-
-### Pen Names
-
-1. Go to BD BookHelper > Pen Names
-2. Add author pen names
-3. Assign to books in the book editor
-
-### Series
-
-1. Go to BD BookHelper > Series (taxonomy)
-2. Create series with name, slug, and description
-3. Assign books to series with volume numbers
-
-### Settings
-
-Configure affiliate codes and integrations under BD BookHelper > Settings.
-
-## Short Links
-
-Books automatically get short URLs based on their title:
-- "Pandemic Hacker" → /ph/
-- "Deal Hunter" → /dh/
-
-Review short links follow the pattern: `{book-slug}r{site-code}`:
-- /phrA = Amazon review
-- /phrG = Goodreads review
-
-## Frequently Asked Questions
-
-### Do I need the theme?
-Yes, this plugin is designed to work with the BDMurphy Bookshelf theme.
-
-### Can I use without affiliate codes?
-Yes, the plugin works without affiliate codes. They are optional.
-
-### What languages are supported?
-English, French, Spanish, German, Italian, Portuguese, Russian, Japanese, and Chinese (Simplified).
-
-## Changelog
-
-### 2.0.0
-- Initial release with full book management, series, pen names, and integrations
diff --git a/bd-bookshelfhelper/uninstall.php b/bd-bookshelfhelper/uninstall.php
deleted file mode 100644
index ae6e15f..0000000
--- a/bd-bookshelfhelper/uninstall.php
+++ /dev/null
@@ -1,39 +0,0 @@
-prefix . 'bd_books',
- $wpdb->prefix . 'bd_book_meta',
- $wpdb->prefix . 'bd_series',
- $wpdb->prefix . 'bd_pen_names',
- $wpdb->prefix . 'bd_reviews',
- $wpdb->prefix . 'bd_ratings',
- $wpdb->prefix . 'bd_review_shortlinks',
-);
-
-foreach ($tables as $table) {
- $wpdb->query("DROP TABLE IF EXISTS {$table}");
-}
-
-// Delete options
-delete_option('bdm_affiliate_codes');
-delete_option('bdm_goodreads_author_id');
-delete_option('bdm_awards');
-
-// Clear scheduled events
-wp_clear_scheduled_hook('bd_bookshelf_goodreads_refresh');
-wp_clear_scheduled_hook('bd_bookshelf_amazon_scrape');
diff --git a/bd-simpleemail/bd-simpleemail.php b/bd-simpleemail/bd-simpleemail.php
deleted file mode 100644
index 924332c..0000000
--- a/bd-simpleemail/bd-simpleemail.php
+++ /dev/null
@@ -1,52 +0,0 @@
- Settings
-
-## Configuration
-
-### Gmail Setup
-
-1. Enable 2-Factor Authentication on your Google account
-2. Generate an App Password: https://support.google.com/accounts/answer/185833
-3. Enter your Gmail address and App Password in plugin settings
-
-### Gemini AI (Optional)
-
-1. Get an API key: https://makersuite.google.com/app/apikey
-2. Enter the API key in plugin settings
-
-### Business Information
-
-Configure your business address in Appearance > Theme Options (required for CAN-SPAM compliance).
-
-## Sending Newsletters
-
-1. Go to BD SimpleEmail > New Newsletter
-2. Enter a subject line
-3. (Optional) Use AI to generate content
-4. Write or edit your newsletter
-5. Click "Send Newsletter"
-
-## Shortcodes
-
-Use `[bdm_signup]` to display a signup form anywhere on your site.
-
-## Frequently Asked Questions
-
-### Do I need to use Gmail?
-Yes, this plugin uses Gmail SMTP for sending. App passwords are required.
-
-### Is AI required?
-No, AI assistance is optional. You can write newsletters manually.
-
-### Is one-click unsubscribe included?
-Yes, all emails include one-click unsubscribe links compliant with Gmail/Yahoo requirements.
-
-## Changelog
-
-### 2.0.0
-- Initial release with subscriber management, AI composition, and Gmail SMTP sending
diff --git a/bd-simpleemail/uninstall.php b/bd-simpleemail/uninstall.php
deleted file mode 100644
index 5d2983d..0000000
--- a/bd-simpleemail/uninstall.php
+++ /dev/null
@@ -1,32 +0,0 @@
-prefix . 'bd_subscribers',
- $wpdb->prefix . 'bd_newsletters',
-);
-
-foreach ($tables as $table) {
- $wpdb->query("DROP TABLE IF EXISTS {$table}");
-}
-
-// Delete options
-delete_option('bdm_gmail_address');
-delete_option('bdm_gmail_password');
-delete_option('bdm_sender_name');
-delete_option('bdm_reply_to');
-delete_option('bdm_gemini_api_key');
diff --git a/bd-bookshelf-plugin/admin/css/bd-bookshelf-admin.css b/umbra-bookshelf/admin/css/umbra-bookshelf-admin.css
similarity index 99%
rename from bd-bookshelf-plugin/admin/css/bd-bookshelf-admin.css
rename to umbra-bookshelf/admin/css/umbra-bookshelf-admin.css
index a9a9d64..ce609bf 100644
--- a/bd-bookshelf-plugin/admin/css/bd-bookshelf-admin.css
+++ b/umbra-bookshelf/admin/css/umbra-bookshelf-admin.css
@@ -1,7 +1,7 @@
/**
* BD Bookshelf - Admin Styles
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
/* Form Layout */
diff --git a/bd-bookshelf-plugin/admin/js/bd-bookshelf-admin.js b/umbra-bookshelf/admin/js/umbra-bookshelf-admin.js
similarity index 94%
rename from bd-bookshelf-plugin/admin/js/bd-bookshelf-admin.js
rename to umbra-bookshelf/admin/js/umbra-bookshelf-admin.js
index 776d8f1..c10fad4 100644
--- a/bd-bookshelf-plugin/admin/js/bd-bookshelf-admin.js
+++ b/umbra-bookshelf/admin/js/umbra-bookshelf-admin.js
@@ -4,7 +4,7 @@
/**
* BD Bookshelf - Admin JavaScript
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
(function($) {
@@ -71,11 +71,11 @@
btn.prop('disabled', true).text('Syncing...');
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_sync_ratings',
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -98,12 +98,12 @@
btn.prop('disabled', true).text('Regenerating...');
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_regenerate_links',
book_id: bookId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -170,12 +170,12 @@
$('body').css('cursor', 'wait');
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: action,
author_id: authorId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
$('body').css('cursor', 'default');
@@ -300,7 +300,7 @@
// Build form data manually for proper nested array handling
var formData = new FormData();
formData.append('action', 'bd_bookshelf_import_scraped');
- formData.append('nonce', bdBookshelfAdmin.nonce);
+ formData.append('nonce', umbraBookshelfAdmin.nonce);
formData.append('source', source);
// Add author data
@@ -326,7 +326,7 @@
});
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: formData,
processData: false,
@@ -365,8 +365,8 @@
}
coverFrame = wp.media({
- title: bdBookshelfAdmin.mediaTitle,
- button: { text: bdBookshelfAdmin.mediaButton },
+ title: umbraBookshelfAdmin.mediaTitle,
+ button: { text: umbraBookshelfAdmin.mediaButton },
multiple: false,
library: { type: 'image' }
});
@@ -458,12 +458,12 @@
// Fetch author data
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_get_author',
id: authorId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -497,12 +497,12 @@
var authorId = $(this).data('id');
if (confirm('Are you sure you want to delete this author?')) {
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_author',
id: authorId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -521,7 +521,7 @@
var data = $(this).serialize();
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_author',
success: function(response) {
@@ -560,12 +560,12 @@
// Fetch series data
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_get_series',
id: seriesId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -586,12 +586,12 @@
var seriesId = $(this).data('id');
if (confirm('Are you sure you want to delete this series? Books in this series will be unlinked.')) {
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_series',
id: seriesId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -610,7 +610,7 @@
var data = $(this).serialize();
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_series',
success: function(response) {
@@ -646,12 +646,12 @@
var shortcodeId = $(this).data('id');
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_get_shortcode',
id: shortcodeId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -674,12 +674,12 @@
var shortcodeId = $(this).data('id');
if (confirm('Are you sure you want to delete this shortcode?')) {
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_shortcode',
id: shortcodeId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -698,7 +698,7 @@
var data = $(this).serialize();
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_shortcode',
success: function(response) {
@@ -720,7 +720,7 @@
var data = form.serialize();
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_book',
success: function(response) {
@@ -741,12 +741,12 @@
*/
function deleteBook(bookId) {
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_book',
id: bookId,
- nonce: bdBookshelfAdmin.nonce
+ nonce: umbraBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
@@ -766,7 +766,7 @@
var data = form.serialize();
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_sites',
success: function(response) {
@@ -787,7 +787,7 @@
var data = form.serialize();
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_settings',
success: function(response) {
@@ -809,7 +809,7 @@
formData.append('action', 'bd_bookshelf_import_csv');
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: formData,
processData: false,
@@ -833,7 +833,7 @@
var data = form.serialize();
$.ajax({
- url: bdBookshelfAdmin.ajaxurl,
+ url: umbraBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_import_posts',
success: function(response) {
diff --git a/umbra-bookshelf/assets/images/default-cover.png b/umbra-bookshelf/assets/images/default-cover.png
new file mode 100644
index 0000000..675bbf4
Binary files /dev/null and b/umbra-bookshelf/assets/images/default-cover.png differ
diff --git a/umbra-bookshelf/docs/ARCHITECTURE.md b/umbra-bookshelf/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..f8ef8e4
--- /dev/null
+++ b/umbra-bookshelf/docs/ARCHITECTURE.md
@@ -0,0 +1,70 @@
+# Architecture
+
+## Why one plugin
+
+Three plugins shipped the same product surface under three admin menus and
+shared two cron hooks. Users installed, updated and deactivated them
+independently while the code assumed all three were present. Consolidating into
+one plugin makes the dependency explicit and lets one activation hook set up a
+consistent state.
+
+The trade-off is that three independent `add_menu_page` calls, three sets of
+admin assets and three activation routines now share a single namespace. Every
+one of those collided, and each collision is recorded in `KNOWN-ISSUES.md`.
+
+## Layers
+
+```
+ umbra-bookshelf.php (bootstrap)
+ ├── constants one UMBRA_BOOKSHELF_* set
+ ├── requires 21 class files, three modules
+ ├── plugins_loaded construct modules
+ ├── init text domain
+ └── admin_menu(9) single top-level menu
+ │
+ ┌──────────────────────────┼──────────────────────────┐
+ │ │ │
+ includes/books includes/library includes/email
+ Umbra_Database Umbra_Bookshelf_Helper Umbra_Simple_Email
+ Umbra_Book_Manager …_Helper_Admin …_Admin
+ Umbra_Authors …_Helper_Install …_Install
+ Umbra_Series Umbra_Email_Sender
+ Umbra_Retail_Sites
+ Umbra_Rating_Sync
+ Umbra_Shortcodes
+ Umbra_SEO assets: admin/, public/, library/, email/
+ Umbra_GDPR assets/
+ Umbra_Goodreads_Scraper
+ Umbra_Amazon_Scraper
+ Umbra_Admin
+ Umbra_Migration
+ Umbra_Theme_Compatibility -> class_alias BD_BookShelfHelper
+```
+
+## Design rules
+
+**One owner per name.** Every class, constant, asset handle, menu slug and AJAX
+action is owned by exactly one module. The bootstrap owns the top-level menu and
+the shared constants; modules own their submenus and assets.
+
+**Persistence is not part of the identity.** Anything written to the database or
+embedded in published content keeps its historical name. The rename covers
+headers, text domains, classes, constants, handles, slugs and JS globals — never
+table names, option keys, meta keys, cron hooks, nonces, shortcodes, post types,
+taxonomies or cookies. See §2.2 of the specification for the full list.
+
+**Registration order is explicit.** The top-level menu registers at
+`admin_menu` priority 9 so the parent exists before modules append submenus at
+the default 10. Modules construct on `plugins_loaded` so their own `init`
+callbacks are registered before `init` fires.
+
+**Compatibility shims are explicit.** `Umbra_Theme_Compatibility` exposes the
+static API the unmodified theme calls, and re-exports the legacy
+`BD_BookShelfHelper` name via `class_alias()`.
+
+## Not resolved here
+
+The catalogue and library modules keep two independent data models that happen
+to share table names `bd_books` and `bd_series`. Consolidation made them
+unconditionally conflict; it did not make them agree. Resolving this is the next
+piece of work — see `KNOWN-ISSUES.md` §1 for the options.
diff --git a/umbra-bookshelf/docs/KNOWN-ISSUES.md b/umbra-bookshelf/docs/KNOWN-ISSUES.md
new file mode 100644
index 0000000..296ad34
--- /dev/null
+++ b/umbra-bookshelf/docs/KNOWN-ISSUES.md
@@ -0,0 +1,132 @@
+# Known issues
+
+Carried into the Umbra Bookshelf consolidation. Items 1–4 are **pre-existing**
+defects inherited from the three merged plugins; item 5 and the two repaired
+below are noted for traceability.
+
+---
+
+## 1. `bd_books` and `bd_series` have two incompatible schemas (DEFERRED — unresolved)
+
+The books and library modules both create tables with identical names and
+different columns.
+
+`Umbra_Database::table()` returns `$wpdb->prefix . 'bd_' . $name`, and the
+catalogue issues `CREATE TABLE {prefix}bd_books` with **no** `IF NOT EXISTS`
+guard:
+
+```
+catalogue wp_bd_books id, title, slug, description, isbn_*, amazon_asin,
+ goodreads_id, rating_*, created_at, ...
+library wp_bd_books id, post_id, pen_name_id, series_id, volume_number,
+ asin, isbn, publish_date, sort_order, created_at
+```
+
+Same for `wp_bd_series` (catalogue: `name`, `slug`, `description` …;
+library: `name`, `slug`, `cover_attachment_id`, `header_attachment_id`, `blurb`, …).
+
+**Consequence.** Whichever module is activated first wins; the second either
+no-ops (`IF NOT EXISTS`, library) or raises a database error (catalogue's
+unguarded `CREATE TABLE`). Whichever schema exists first is what the other
+module then reads and writes against, producing wrong results rather than an
+obvious failure.
+
+**Why deferred.** Resolving it means choosing a canonical schema and migrating
+live data — a data-migration project, not a rename. Both datasets may hold rows
+that matter.
+
+**Options.**
+
+1. Library schema becomes canonical; catalogue tables move to `bd_bookshelf_*`
+ with an upgrade routine copying existing rows. Requires renaming DB keys,
+ which this consolidation deliberately avoids.
+2. Catalogue schema becomes canonical; library relies on the `book` post type and
+ postmeta instead of `bd_books`.
+3. Delete the catalogue duplicates and keep only the library model.
+
+---
+
+## 2. Goodreads and Amazon scraping is implemented twice
+
+* Catalogue: `Umbra_Goodreads_Scraper`, `Umbra_Amazon_Scraper`.
+* Library: `Umbra_Bookshelf_Helper::refresh_goodreads_data()`,
+ `::scrape_amazon_ratings()`.
+
+Both attach to the **same** cron hooks (`bd_bookshelf_goodreads_refresh`,
+`bd_bookshelf_amazon_scrape`), so each scheduled run performs duplicate
+requests to third-party APIs — wasted quota, faster rate limiting, and two
+places to fix a parsing bug. Needs a decision on which implementation survives.
+
+---
+
+## 3. `assets/images/default-cover.png` was missing
+
+`Umbra_Bookshelf_Helper` returns `assets/images/default-cover.png` as the
+fallback cover, but the repository contained no images at all, so every
+cover-less book rendered a broken image.
+
+**Repaired during consolidation:** a neutral 600×900 placeholder PNG was
+generated at that path. Replace it with real artwork before release.
+
+---
+
+## 4. `email/assets/js/admin.js` did not exist
+
+`Umbra_Simple_Email_Admin::enqueue_assets()` enqueued it on every email admin
+page, but no such file was ever committed — a guaranteed 404.
+
+**Repaired during consolidation:** an empty placeholder was created with a
+comment explaining the history. Add the intended behaviour, or drop the enqueue.
+
+---
+
+## 5. `Umbra_SEO` is never instantiated
+
+`Umbra_SEO` registers its hooks from a constructor, but the bootstrap does not
+construct it — carried over unchanged from the original BD Bookshelf bootstrap.
+The class is loaded and its hooks never attach, so the module is inert.
+
+Left as-is because enabling it changes behaviour. Either instantiate it in
+`Umbra_Bookshelf::init_modules()` or delete it.
+
+---
+
+## 6. Licence metadata is inconsistent (NOT CHANGED)
+
+Plugin headers declared `License: GPLv2 or later` while the repository's
+`LICENSE`/`NOTICE` files (added in `85de97b`) and every SPDX header state
+**Apache-2.0**. The consolidated header now says Apache-2.0 to match the SPDX
+headers and `LICENSE`, but this needs a deliberate decision — WordPress
+plugin-directory submissions require a GPL-compatible licence.
+
+---
+
+## Fixed during consolidation
+
+* **`Cannot declare class` fatal.** `class-bdm-theme-compatibility.php` declared
+ `BD_BookShelfHelper`, the same class name as the library plugin's own helper.
+ Latent while the two were separate plugins (it only fired when both were
+ active), but an unconditional fatal once merged. The shim is now
+ `Umbra_Theme_Compatibility`, with a `class_alias()` preserving the legacy name
+ for the unmodified theme.
+* **Duplicate `bd_bookshelf_import_csv` AJAX handler.** Both `Umbra_Admin` and
+ `Umbra_Migration` registered it; the second silently replaced the first. The
+ import page now lives solely in `Umbra_Admin` (it is the variant wired to
+ `admin/js/umbra-bookshelf-admin.js`); `Umbra_Migration` keeps only the
+ preview/run endpoints it uniquely provides.
+* **Duplicate `bd-bookshelf-settings` submenu.** Catalogue and library both
+ claimed the slug, leaving one Settings screen unreachable. Now
+ `umbra-bookshelf-settings` and `umbra-bookshelf-library-settings`.
+* **Duplicate admin menu.** All three modules called `add_menu_page`. Now one
+ top-level menu registered by the bootstrap at priority 9.
+* **Colliding asset handles.** Catalogue and library both enqueued
+ `bd-bookshelf-admin`; the second enqueue was silently discarded. Handles are
+ now unique per module.
+* **Modules could miss their own `init` callbacks.** Constructing modules from
+ inside an `init` callback means their `init` registrations race the hook
+ currently being dispatched. Modules are now constructed on `plugins_loaded`,
+ behind a re-entrancy guard.
+* **Over-broad asset hook check.** The library module's
+ `strpos($hook, 'bd-bookshelf')` matched every catalogue and email screen once
+ all slugs shared the `umbra-bookshelf-` prefix. Now matched against its own
+ slug list.
diff --git a/bd-simpleemail/assets/css/admin.css b/umbra-bookshelf/email/assets/css/admin.css
similarity index 100%
rename from bd-simpleemail/assets/css/admin.css
rename to umbra-bookshelf/email/assets/css/admin.css
diff --git a/umbra-bookshelf/email/assets/js/admin.js b/umbra-bookshelf/email/assets/js/admin.js
new file mode 100644
index 0000000..1a534f0
--- /dev/null
+++ b/umbra-bookshelf/email/assets/js/admin.js
@@ -0,0 +1,11 @@
+// Copyright 2026 Umbra Tools (umbratools.com)
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * Umbra Bookshelf - Email module admin scripts.
+ *
+ * NOTE: Umbra_Simple_Email_Admin::enqueue_assets() has always enqueued this
+ * file, but the file was never present in the source repository, so every
+ * email admin page issued a 404 for it. It is created here as an empty
+ * placeholder so the enqueue resolves. Add email-module admin behaviour below.
+ */
diff --git a/bd-bookshelf-plugin/includes/class-bdm-admin.php b/umbra-bookshelf/includes/books/class-umbra-admin.php
similarity index 81%
rename from bd-bookshelf-plugin/includes/class-bdm-admin.php
rename to umbra-bookshelf/includes/books/class-umbra-admin.php
index fd1b912..7ddc220 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-admin.php
+++ b/umbra-bookshelf/includes/books/class-umbra-admin.php
@@ -5,14 +5,14 @@
/**
* Admin Interface class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Admin {
+class Umbra_Admin {
/**
* Constructor
@@ -44,44 +44,37 @@ class BDM_Admin {
* Add admin menu
*/
public function add_admin_menu() {
- add_menu_page(
- __('Books', 'bd-bookshelf'),
- __('Books', 'bd-bookshelf'),
- 'manage_options',
- 'bd-bookshelf',
- [$this, 'render_books_page'],
- 'dashicons-book',
- 30
- );
-
- add_submenu_page('bd-bookshelf', __('All Books', 'bd-bookshelf'), __('All Books', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf', [$this, 'render_books_page']);
- add_submenu_page('bd-bookshelf', __('Add New Book', 'bd-bookshelf'), __('Add New Book', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-add', [$this, 'render_book_form']);
- add_submenu_page('bd-bookshelf', __('Authors', 'bd-bookshelf'), __('Authors', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-authors', [$this, 'render_authors_page']);
- add_submenu_page('bd-bookshelf', __('Series', 'bd-bookshelf'), __('Series', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-series', [$this, 'render_series_page']);
- add_submenu_page('bd-bookshelf', __('Retail Sites', 'bd-bookshelf'), __('Retail Sites', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-sites', [$this, 'render_sites_page']);
- add_submenu_page('bd-bookshelf', __('Shortcodes', 'bd-bookshelf'), __('Shortcodes', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-shortcodes', [$this, 'render_shortcodes_page']);
- add_submenu_page('bd-bookshelf', __('Rating Sync', 'bd-bookshelf'), __('Rating Sync', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-sync', [$this, 'render_sync_page']);
- add_submenu_page('bd-bookshelf', __('Settings', 'bd-bookshelf'), __('Settings', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-settings', [$this, 'render_settings_page']);
- add_submenu_page('bd-bookshelf', __('Import', 'bd-bookshelf'), __('Import', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-import', [$this, 'render_import_page']);
+ // The top-level "Umbra Bookshelf" menu is registered once by the plugin
+ // bootstrap (Umbra_Bookshelf::register_admin_menu); each module only
+ // contributes submenus beneath it.
+ add_submenu_page('umbra-bookshelf', __('All Books', 'umbra-bookshelf'), __('All Books', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-all', [$this, 'render_books_page']);
+ add_submenu_page('umbra-bookshelf', __('Add New Book', 'umbra-bookshelf'), __('Add New Book', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-add', [$this, 'render_book_form']);
+ add_submenu_page('umbra-bookshelf', __('Authors', 'umbra-bookshelf'), __('Authors', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-authors', [$this, 'render_authors_page']);
+ add_submenu_page('umbra-bookshelf', __('Series', 'umbra-bookshelf'), __('Series', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-series', [$this, 'render_series_page']);
+ add_submenu_page('umbra-bookshelf', __('Retail Sites', 'umbra-bookshelf'), __('Retail Sites', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-sites', [$this, 'render_sites_page']);
+ add_submenu_page('umbra-bookshelf', __('Shortcodes', 'umbra-bookshelf'), __('Shortcodes', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-shortcodes', [$this, 'render_shortcodes_page']);
+ add_submenu_page('umbra-bookshelf', __('Rating Sync', 'umbra-bookshelf'), __('Rating Sync', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-sync', [$this, 'render_sync_page']);
+ add_submenu_page('umbra-bookshelf', __('Import', 'umbra-bookshelf'), __('Import', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-import', [$this, 'render_import_page']);
+ add_submenu_page('umbra-bookshelf', __('Settings', 'umbra-bookshelf'), __('Settings', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-settings', [$this, 'render_settings_page']);
}
/**
* Enqueue admin scripts
*/
public function enqueue_admin_scripts($hook) {
- if (strpos($hook, 'bd-bookshelf') === false) {
+ if (strpos($hook, 'umbra-bookshelf') === false) {
return;
}
wp_enqueue_media();
- wp_enqueue_style('bd-bookshelf-admin', BD_BOOKSHELF_URL . 'admin/css/bd-bookshelf-admin.css', [], BD_BOOKSHELF_VERSION);
- wp_enqueue_script('bd-bookshelf-admin', BD_BOOKSHELF_URL . 'admin/js/bd-bookshelf-admin.js', ['jquery'], BD_BOOKSHELF_VERSION, true);
+ wp_enqueue_style('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/css/umbra-bookshelf-admin.css', [], UMBRA_BOOKSHELF_VERSION);
+ wp_enqueue_script('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/js/umbra-bookshelf-admin.js', ['jquery'], UMBRA_BOOKSHELF_VERSION, true);
- wp_localize_script('bd-bookshelf-admin', 'bdBookshelfAdmin', [
+ wp_localize_script('umbra-bookshelf-admin', 'umbraBookshelfAdmin', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('bd_bookshelf_admin_nonce'),
- 'mediaTitle' => __('Select Cover Image', 'bd-bookshelf'),
- 'mediaButton' => __('Use This Image', 'bd-bookshelf'),
+ 'mediaTitle' => __('Select Cover Image', 'umbra-bookshelf'),
+ 'mediaButton' => __('Use This Image', 'umbra-bookshelf'),
]);
}
@@ -90,29 +83,29 @@ class BDM_Admin {
*/
public function render_books_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
- $books = BDM_Book_Manager::get_books(['number' => 50]);
+ $books = Umbra_Book_Manager::get_books(['number' => 50]);
?>
@@ -154,45 +147,45 @@ class BDM_Admin {
*/
public function render_book_form() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
$book_id = isset($_GET['book_id']) ? absint($_GET['book_id']) : 0;
- $book = $book_id ? BDM_Book_Manager::get_book($book_id) : null;
- $series = BDM_Series::get_series(['number' => 100]);
- $authors = BDM_Authors::get_authors(['number' => 100]);
+ $book = $book_id ? Umbra_Book_Manager::get_book($book_id) : null;
+ $series = Umbra_Series::get_series(['number' => 100]);
+ $authors = Umbra_Authors::get_authors(['number' => 100]);
// Get existing retail links grouped by format and site
$existing_links = [];
if ($book) {
- $raw_links = BDM_Book_Manager::get_book_retail_links($book_id);
+ $raw_links = Umbra_Book_Manager::get_book_retail_links($book_id);
foreach ($raw_links as $link) {
$existing_links[$link->format][$link->site_name] = $link->url;
}
}
// Retail sites for URL inputs
- $retail_sites = BDM_Retail_Sites::get_sites(true);
+ $retail_sites = Umbra_Retail_Sites::get_sites(true);
?>
-
+
@@ -334,29 +327,29 @@ class BDM_Admin {
*/
public function render_authors_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
- $authors = BDM_Authors::get_authors(['number' => 100]);
+ $authors = Umbra_Authors::get_authors(['number' => 100]);
?>
@@ -430,35 +423,35 @@ class BDM_Admin {
*/
public function render_series_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
- $series = BDM_Series::get_series(['number' => 100]);
+ $series = Umbra_Series::get_series(['number' => 100]);
?>
-
-
+
+
-
-
-
+
+
+
-
+
name); ?>
description, 15)); ?>
- |
-
+ |
+
@@ -471,7 +464,7 @@ class BDM_Admin {
-
+
@@ -501,14 +494,14 @@ class BDM_Admin {
*/
public function render_sites_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
- $sites = BDM_Retail_Sites::get_sites(false);
+ $sites = Umbra_Retail_Sites::get_sites(false);
?>
-
-
+
+
@@ -549,33 +542,33 @@ class BDM_Admin {
*/
public function render_shortcodes_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
global $wpdb;
- $table = BDM_Database::table('shortcodes');
+ $table = Umbra_Database::table('shortcodes');
$shortcodes = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC");
?>
-
-
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
@@ -584,8 +577,8 @@ class BDM_Admin {
redirect_type); ?>
click_count); ?>
- |
-
+ |
+
@@ -598,7 +591,7 @@ class BDM_Admin {
-
+
@@ -645,42 +638,42 @@ class BDM_Admin {
*/
public function render_sync_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
- $last_sync = BDM_Rating_Sync::get_last_sync_info();
+ $last_sync = Umbra_Rating_Sync::get_last_sync_info();
?>
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -692,28 +685,28 @@ class BDM_Admin {
*/
public function render_settings_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
?>
-
+
@@ -725,23 +718,23 @@ class BDM_Admin {
*/
public function render_import_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
?>
-
+
-
-
+
+
-
+
-
-
+
+
@@ -825,10 +818,10 @@ class BDM_Admin {
];
if ($book_id) {
- $result = BDM_Book_Manager::update_book($book_id, $data);
+ $result = Umbra_Book_Manager::update_book($book_id, $data);
} else {
$data['slug'] = sanitize_title($data['title']);
- $book_id = BDM_Book_Manager::create_book($data);
+ $book_id = Umbra_Book_Manager::create_book($data);
$result = $book_id !== false;
}
@@ -849,8 +842,8 @@ class BDM_Admin {
private function save_retail_urls($book_id, $retail_urls) {
global $wpdb;
- $links_table = BDM_Database::table('retail_links');
- $sites_table = BDM_Database::table('retail_sites');
+ $links_table = Umbra_Database::table('retail_links');
+ $sites_table = Umbra_Database::table('retail_sites');
// Delete existing non-custom links
$wpdb->delete($links_table, ['book_id' => $book_id, 'is_custom' => 0], ['%d', '%d']);
@@ -910,7 +903,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid book ID']);
}
- $result = BDM_Book_Manager::delete_book($book_id);
+ $result = Umbra_Book_Manager::delete_book($book_id);
if ($result) {
wp_send_json_success(['message' => 'Book deleted']);
@@ -934,7 +927,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid author ID']);
}
- $author = BDM_Authors::get_author($author_id);
+ $author = Umbra_Authors::get_author($author_id);
if (!$author) {
wp_send_json_error(['message' => 'Author not found']);
}
@@ -964,9 +957,9 @@ class BDM_Admin {
];
if ($author_id) {
- $result = BDM_Authors::update_author($author_id, $data);
+ $result = Umbra_Authors::update_author($author_id, $data);
} else {
- $author_id = BDM_Authors::create_author($data);
+ $author_id = Umbra_Authors::create_author($data);
$result = $author_id !== false;
}
@@ -992,7 +985,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid author ID']);
}
- $result = BDM_Authors::delete_author($author_id);
+ $result = Umbra_Authors::delete_author($author_id);
if ($result) {
wp_send_json_success(['message' => 'Author deleted']);
@@ -1016,7 +1009,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid series ID']);
}
- $series = BDM_Series::get_series_by_id($series_id);
+ $series = Umbra_Series::get_series_by_id($series_id);
if (!$series) {
wp_send_json_error(['message' => 'Series not found']);
}
@@ -1042,9 +1035,9 @@ class BDM_Admin {
];
if ($series_id) {
- $result = BDM_Series::update_series($series_id, $data);
+ $result = Umbra_Series::update_series($series_id, $data);
} else {
- $series_id = BDM_Series::create_series($data);
+ $series_id = Umbra_Series::create_series($data);
$result = $series_id !== false;
}
@@ -1070,7 +1063,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid series ID']);
}
- $result = BDM_Series::delete_series($series_id);
+ $result = Umbra_Series::delete_series($series_id);
if ($result) {
wp_send_json_success(['message' => 'Series deleted']);
@@ -1095,7 +1088,7 @@ class BDM_Admin {
}
global $wpdb;
- $table = BDM_Database::table('shortcodes');
+ $table = Umbra_Database::table('shortcodes');
$shortcode = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
@@ -1134,9 +1127,9 @@ class BDM_Admin {
}
if ($shortcode_id) {
- $result = BDM_Shortcodes::update_shortcode($shortcode_id, $data);
+ $result = Umbra_Shortcodes::update_shortcode($shortcode_id, $data);
} else {
- $shortcode_id = BDM_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']);
+ $shortcode_id = Umbra_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']);
$result = $shortcode_id !== false;
}
@@ -1162,7 +1155,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid shortcode ID']);
}
- $result = BDM_Shortcodes::delete_shortcode($shortcode_id);
+ $result = Umbra_Shortcodes::delete_shortcode($shortcode_id);
if ($result) {
wp_send_json_success(['message' => 'Shortcode deleted']);
@@ -1183,7 +1176,7 @@ class BDM_Admin {
if (!empty($_POST['affiliate'])) {
global $wpdb;
- $table = BDM_Database::table('retail_sites');
+ $table = Umbra_Database::table('retail_sites');
foreach ($_POST['affiliate'] as $site_id => $code) {
$wpdb->update(
@@ -1209,7 +1202,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
- BDM_GDPR::update_consent_text($_POST['gdpr_text'] ?? '');
+ Umbra_GDPR::update_consent_text($_POST['gdpr_text'] ?? '');
wp_send_json_success(['message' => 'Settings saved']);
}
@@ -1252,7 +1245,7 @@ class BDM_Admin {
}
// Check if already imported - update instead of skip
- $existing = BDM_Book_Manager::get_book_by_title($data['title']);
+ $existing = Umbra_Book_Manager::get_book_by_title($data['title']);
$book_data = [
'title' => $data['title'],
@@ -1276,14 +1269,14 @@ class BDM_Admin {
}
}
if (!empty($update_data)) {
- BDM_Book_Manager::update_book($existing->id, $update_data);
+ Umbra_Book_Manager::update_book($existing->id, $update_data);
$updated++;
} else {
$skipped++;
}
} else {
// Create new book
- $book_id = BDM_Book_Manager::create_book($book_data);
+ $book_id = Umbra_Book_Manager::create_book($book_data);
if ($book_id) {
$imported++;
} else {
@@ -1332,14 +1325,14 @@ class BDM_Admin {
foreach ($posts as $post) {
// Check if already imported
- $existing = BDM_Book_Manager::get_book_by_title($post->post_title);
+ $existing = Umbra_Book_Manager::get_book_by_title($post->post_title);
if ($existing) {
$skipped++;
continue;
}
// Import book
- $book_id = BDM_Book_Manager::create_book([
+ $book_id = Umbra_Book_Manager::create_book([
'title' => $post->post_title,
'description' => $post->post_content,
]);
@@ -1368,7 +1361,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
- $result = BDM_Rating_Sync::manual_sync();
+ $result = Umbra_Rating_Sync::manual_sync();
wp_send_json_success([
'message' => sprintf('Synced %d books, failed %d', $result['synced'], $result['failed']),
diff --git a/bd-bookshelf-plugin/includes/class-bdm-amazon-scraper.php b/umbra-bookshelf/includes/books/class-umbra-amazon-scraper.php
similarity index 99%
rename from bd-bookshelf-plugin/includes/class-bdm-amazon-scraper.php
rename to umbra-bookshelf/includes/books/class-umbra-amazon-scraper.php
index 1bd275d..4e91948 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-amazon-scraper.php
+++ b/umbra-bookshelf/includes/books/class-umbra-amazon-scraper.php
@@ -7,14 +7,14 @@
*
* Fetches author and book data from Amazon
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Amazon_Scraper {
+class Umbra_Amazon_Scraper {
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
diff --git a/bd-bookshelf-plugin/includes/class-bdm-authors.php b/umbra-bookshelf/includes/books/class-umbra-authors.php
similarity index 90%
rename from bd-bookshelf-plugin/includes/class-bdm-authors.php
rename to umbra-bookshelf/includes/books/class-umbra-authors.php
index a51b2fa..788349c 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-authors.php
+++ b/umbra-bookshelf/includes/books/class-umbra-authors.php
@@ -5,14 +5,14 @@
/**
* Authors Management class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Authors {
+class Umbra_Authors {
/**
* Get all authors
@@ -20,7 +20,7 @@ class BDM_Authors {
public static function get_authors($args = []) {
global $wpdb;
- $table = BDM_Database::table('authors');
+ $table = Umbra_Database::table('authors');
$defaults = [
'number' => 20,
@@ -59,7 +59,7 @@ class BDM_Authors {
public static function get_author($author_id) {
global $wpdb;
- $table = BDM_Database::table('authors');
+ $table = Umbra_Database::table('authors');
return $wpdb->get_row(
$wpdb->prepare(
@@ -75,7 +75,7 @@ class BDM_Authors {
public static function create_author($data) {
global $wpdb;
- $table = BDM_Database::table('authors');
+ $table = Umbra_Database::table('authors');
$result = $wpdb->insert($table, [
'display_name' => sanitize_text_field($data['display_name']),
@@ -95,7 +95,7 @@ class BDM_Authors {
public static function update_author($author_id, $data) {
global $wpdb;
- $table = BDM_Database::table('authors');
+ $table = Umbra_Database::table('authors');
$update_data = [];
@@ -145,10 +145,10 @@ class BDM_Authors {
global $wpdb;
// Remove from book_authors
- $wpdb->delete(BDM_Database::table('book_authors'), ['author_id' => $author_id], ['%d']);
+ $wpdb->delete(Umbra_Database::table('book_authors'), ['author_id' => $author_id], ['%d']);
// Delete author
- $table = BDM_Database::table('authors');
+ $table = Umbra_Database::table('authors');
return $wpdb->delete($table, ['id' => $author_id], ['%d']) !== false;
}
@@ -158,7 +158,7 @@ class BDM_Authors {
public static function count_authors($search = '') {
global $wpdb;
- $table = BDM_Database::table('authors');
+ $table = Umbra_Database::table('authors');
$where = '1=1';
$params = [];
diff --git a/bd-bookshelf-plugin/includes/class-bdm-book-manager.php b/umbra-bookshelf/includes/books/class-umbra-book-manager.php
similarity index 91%
rename from bd-bookshelf-plugin/includes/class-bdm-book-manager.php
rename to umbra-bookshelf/includes/books/class-umbra-book-manager.php
index b0a5cd9..76e735b 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-book-manager.php
+++ b/umbra-bookshelf/includes/books/class-umbra-book-manager.php
@@ -5,14 +5,14 @@
/**
* Book Manager class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Book_Manager {
+class Umbra_Book_Manager {
/**
* Get all books
@@ -20,7 +20,7 @@ class BDM_Book_Manager {
public static function get_books($args = []) {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$defaults = [
'number' => 20,
@@ -63,7 +63,7 @@ class BDM_Book_Manager {
public static function get_book($book_id) {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$book = $wpdb->get_row(
$wpdb->prepare(
@@ -75,7 +75,7 @@ class BDM_Book_Manager {
if ($book) {
$book->authors = self::get_book_authors($book_id);
$book->retail_links = self::get_book_retail_links($book_id);
- $book->seo = BDM_SEO::get_book_seo($book_id);
+ $book->seo = Umbra_SEO::get_book_seo($book_id);
}
return $book;
@@ -87,7 +87,7 @@ class BDM_Book_Manager {
public static function get_book_by_title($title) {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$book = $wpdb->get_row(
$wpdb->prepare(
@@ -105,7 +105,7 @@ class BDM_Book_Manager {
public static function get_book_by_slug($slug) {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$book = $wpdb->get_row(
$wpdb->prepare(
@@ -117,7 +117,7 @@ class BDM_Book_Manager {
if ($book) {
$book->authors = self::get_book_authors($book->id);
$book->retail_links = self::get_book_retail_links($book->id);
- $book->seo = BDM_SEO::get_book_seo($book->id);
+ $book->seo = Umbra_SEO::get_book_seo($book->id);
}
return $book;
@@ -129,7 +129,7 @@ class BDM_Book_Manager {
public static function create_book($data) {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$slug = sanitize_title($data['slug'] ?? $data['title'] ?? '');
@@ -184,7 +184,7 @@ class BDM_Book_Manager {
public static function update_book($book_id, $data) {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$update_data = [];
@@ -274,12 +274,12 @@ class BDM_Book_Manager {
global $wpdb;
// Delete related data
- $wpdb->delete(BDM_Database::table('book_authors'), ['book_id' => $book_id], ['%d']);
- $wpdb->delete(BDM_Database::table('retail_links'), ['book_id' => $book_id], ['%d']);
- $wpdb->delete(BDM_Database::table('book_seo'), ['book_id' => $book_id], ['%d']);
+ $wpdb->delete(Umbra_Database::table('book_authors'), ['book_id' => $book_id], ['%d']);
+ $wpdb->delete(Umbra_Database::table('retail_links'), ['book_id' => $book_id], ['%d']);
+ $wpdb->delete(Umbra_Database::table('book_seo'), ['book_id' => $book_id], ['%d']);
// Delete book
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$result = $wpdb->delete($table, ['id' => $book_id], ['%d']);
return $result !== false;
@@ -291,7 +291,7 @@ class BDM_Book_Manager {
public static function get_book_authors($book_id) {
global $wpdb;
- $table = BDM_Database::table('book_authors');
+ $table = Umbra_Database::table('book_authors');
$authors = $wpdb->get_results(
$wpdb->prepare(
@@ -309,8 +309,8 @@ class BDM_Book_Manager {
public static function update_book_authors_by_id($book_id, $author_ids) {
global $wpdb;
- $table = BDM_Database::table('book_authors');
- $authors_table = BDM_Database::table('authors');
+ $table = Umbra_Database::table('book_authors');
+ $authors_table = Umbra_Database::table('authors');
// Delete existing authors
$wpdb->delete($table, ['book_id' => $book_id], ['%d']);
@@ -349,7 +349,7 @@ class BDM_Book_Manager {
public static function update_book_authors($book_id, $authors) {
global $wpdb;
- $table = BDM_Database::table('book_authors');
+ $table = Umbra_Database::table('book_authors');
// Delete existing authors
$wpdb->delete($table, ['book_id' => $book_id], ['%d']);
@@ -373,8 +373,8 @@ class BDM_Book_Manager {
public static function get_book_retail_links($book_id, $format = '') {
global $wpdb;
- $table = BDM_Database::table('retail_links');
- $sites_table = BDM_Database::table('retail_sites');
+ $table = Umbra_Database::table('retail_links');
+ $sites_table = Umbra_Database::table('retail_sites');
$where = 'rl.book_id = %d AND rs.is_active = 1';
$params = [$book_id];
@@ -404,7 +404,7 @@ class BDM_Book_Manager {
public static function get_books_by_series($series_id) {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$books = $wpdb->get_results(
$wpdb->prepare(
@@ -429,7 +429,7 @@ class BDM_Book_Manager {
public static function get_series_list() {
global $wpdb;
- $table = BDM_Database::table('series');
+ $table = Umbra_Database::table('series');
return $wpdb->get_results("SELECT * FROM {$table} ORDER BY name ASC");
}
@@ -440,7 +440,7 @@ class BDM_Book_Manager {
public static function count_books($search = '') {
global $wpdb;
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$where = '1=1';
$params = [];
diff --git a/bd-bookshelf-plugin/includes/class-bdm-database.php b/umbra-bookshelf/includes/books/class-umbra-database.php
similarity index 98%
rename from bd-bookshelf-plugin/includes/class-bdm-database.php
rename to umbra-bookshelf/includes/books/class-umbra-database.php
index d2fd48b..b6e9e9a 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-database.php
+++ b/umbra-bookshelf/includes/books/class-umbra-database.php
@@ -5,14 +5,14 @@
/**
* Database management class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Database {
+class Umbra_Database {
/**
* Get table names with prefix
@@ -187,7 +187,7 @@ class BDM_Database {
$this->insert_default_sites();
// Store schema version
- update_option('bd_bookshelf_db_version', BD_BOOKSHELF_VERSION);
+ update_option('bd_bookshelf_db_version', UMBRA_BOOKSHELF_VERSION);
}
/**
diff --git a/bd-bookshelf-plugin/includes/class-bdm-gdpr.php b/umbra-bookshelf/includes/books/class-umbra-gdpr.php
similarity index 90%
rename from bd-bookshelf-plugin/includes/class-bdm-gdpr.php
rename to umbra-bookshelf/includes/books/class-umbra-gdpr.php
index bfb6531..3599163 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-gdpr.php
+++ b/umbra-bookshelf/includes/books/class-umbra-gdpr.php
@@ -5,14 +5,14 @@
/**
* GDPR Compliance class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_GDPR {
+class Umbra_GDPR {
/**
* Constructor
@@ -35,14 +35,14 @@ class BDM_GDPR {
}
wp_enqueue_script(
- 'bd-bookshelf-gdpr',
- BD_BOOKSHELF_URL . 'public/js/bd-bookshelf-gdpr.js',
+ 'umbra-bookshelf-gdpr',
+ UMBRA_BOOKSHELF_URL . 'public/js/umbra-bookshelf-gdpr.js',
[],
- BD_BOOKSHELF_VERSION,
+ UMBRA_BOOKSHELF_VERSION,
true
);
- wp_localize_script('bd-bookshelf-gdpr', 'bdBookshelfGdpr', [
+ wp_localize_script('umbra-bookshelf-gdpr', 'umbraBookshelfGdpr', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('bd_bookshelf_gdpr_nonce'),
'cookieDays' => 30,
@@ -69,7 +69,7 @@ class BDM_GDPR {
echo '
';
echo '
';
echo '
' . wp_kses_post($privacy_text) . '
';
- echo '';
+ echo '';
echo '
';
echo '
';
}
diff --git a/bd-bookshelf-plugin/includes/class-bdm-goodreads-scraper.php b/umbra-bookshelf/includes/books/class-umbra-goodreads-scraper.php
similarity index 99%
rename from bd-bookshelf-plugin/includes/class-bdm-goodreads-scraper.php
rename to umbra-bookshelf/includes/books/class-umbra-goodreads-scraper.php
index 0e4aef4..3ef21c9 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-goodreads-scraper.php
+++ b/umbra-bookshelf/includes/books/class-umbra-goodreads-scraper.php
@@ -7,14 +7,14 @@
*
* Fetches author and book data from Goodreads
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Goodreads_Scraper {
+class Umbra_Goodreads_Scraper {
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
diff --git a/bd-bookshelf-plugin/includes/class-bdm-migration.php b/umbra-bookshelf/includes/books/class-umbra-migration.php
similarity index 82%
rename from bd-bookshelf-plugin/includes/class-bdm-migration.php
rename to umbra-bookshelf/includes/books/class-umbra-migration.php
index 2a9818b..8605efd 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-migration.php
+++ b/umbra-bookshelf/includes/books/class-umbra-migration.php
@@ -5,39 +5,31 @@
/**
* Migration Manager class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Migration {
+class Umbra_Migration {
/**
* Constructor
+ *
+ * The "Import" admin page itself is registered once by Umbra_Admin
+ * (slug: umbra-bookshelf-import), which is the variant wired to
+ * admin/js/umbra-bookshelf-admin.js. This class only supplies the
+ * two-phase preview/run AJAX endpoints that page has no equivalent for.
+ *
+ * Note: the bd_bookshelf_import_csv action is intentionally NOT
+ * registered here -- Umbra_Admin already owns that action. Registering
+ * it twice silently overwrote one of the two handlers.
*/
public function __construct() {
- add_action('admin_menu', [$this, 'add_admin_menu']);
-
// AJAX handlers
add_action('wp_ajax_bd_bookshelf_preview_import', [$this, 'ajax_import_posts']);
add_action('wp_ajax_bd_bookshelf_run_import', [$this, 'ajax_run_import_posts']);
- add_action('wp_ajax_bd_bookshelf_import_csv', [$this, 'ajax_import_csv']);
- }
-
- /**
- * Add admin menu
- */
- public function add_admin_menu() {
- add_submenu_page(
- 'bd-bookshelf',
- __('Import Books', 'bd-bookshelf'),
- __('Import', 'bd-bookshelf'),
- 'manage_options',
- 'bd-bookshelf-import',
- [$this, 'render_import_page']
- );
}
/**
@@ -45,33 +37,33 @@ class BDM_Migration {
*/
public function render_import_page() {
if (!current_user_can('manage_options')) {
- wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
+ wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
?>
-
+
-
-
+
+
-
+
-
+
-
-
+
+
@@ -174,14 +166,14 @@ class BDM_Migration {
foreach ($posts as $post) {
// Check if already imported
- $existing = BDM_Book_Manager::get_book_by_title($post->post_title);
+ $existing = Umbra_Book_Manager::get_book_by_title($post->post_title);
if ($existing) {
$skipped++;
continue;
}
// Import book
- $book_id = BDM_Book_Manager::create_book([
+ $book_id = Umbra_Book_Manager::create_book([
'title' => $post->post_title,
'description' => $post->post_content,
]);
@@ -237,14 +229,14 @@ class BDM_Migration {
}
// Check if already imported
- $existing = BDM_Book_Manager::get_book_by_title($data['title']);
+ $existing = Umbra_Book_Manager::get_book_by_title($data['title']);
if ($existing) {
$skipped++;
continue;
}
// Create book
- $book_id = BDM_Book_Manager::create_book([
+ $book_id = Umbra_Book_Manager::create_book([
'title' => $data['title'],
'description' => $data['description'] ?? '',
'isbn_ebook' => $data['isbn_ebook'] ?? '',
diff --git a/bd-bookshelf-plugin/includes/class-bdm-rating-sync.php b/umbra-bookshelf/includes/books/class-umbra-rating-sync.php
similarity index 95%
rename from bd-bookshelf-plugin/includes/class-bdm-rating-sync.php
rename to umbra-bookshelf/includes/books/class-umbra-rating-sync.php
index 0483e3f..348a05c 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-rating-sync.php
+++ b/umbra-bookshelf/includes/books/class-umbra-rating-sync.php
@@ -5,14 +5,14 @@
/**
* Rating Sync Engine class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Rating_Sync {
+class Umbra_Rating_Sync {
/**
* Constructor
@@ -36,7 +36,7 @@ class BDM_Rating_Sync {
public function add_cron_interval($schedules) {
$schedules['bd_bookshelf_twelve_hours'] = [
'interval' => 43200, // 12 hours in seconds
- 'display' => esc_html__('Every 12 Hours', 'bd-bookshelf'),
+ 'display' => esc_html__('Every 12 Hours', 'umbra-bookshelf'),
];
return $schedules;
@@ -46,7 +46,7 @@ class BDM_Rating_Sync {
* Sync all book ratings
*/
public function sync_all_ratings() {
- $books = BDM_Book_Manager::get_books(['number' => 999]);
+ $books = Umbra_Book_Manager::get_books(['number' => 999]);
$synced = 0;
$failed = 0;
@@ -81,7 +81,7 @@ class BDM_Rating_Sync {
public function sync_book_rating($book_id) {
global $wpdb;
- $book = BDM_Book_Manager::get_book($book_id);
+ $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) {
return false;
}
@@ -111,7 +111,7 @@ class BDM_Rating_Sync {
}
// Update book ratings
- $table = BDM_Database::table('books');
+ $table = Umbra_Database::table('books');
$wpdb->update(
$table,
[
diff --git a/bd-bookshelf-plugin/includes/class-bdm-retail-sites.php b/umbra-bookshelf/includes/books/class-umbra-retail-sites.php
similarity index 92%
rename from bd-bookshelf-plugin/includes/class-bdm-retail-sites.php
rename to umbra-bookshelf/includes/books/class-umbra-retail-sites.php
index 32f3f09..b56038f 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-retail-sites.php
+++ b/umbra-bookshelf/includes/books/class-umbra-retail-sites.php
@@ -5,14 +5,14 @@
/**
* Retail Sites management class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Retail_Sites {
+class Umbra_Retail_Sites {
/**
* Get all retail sites
@@ -20,7 +20,7 @@ class BDM_Retail_Sites {
public static function get_sites($active_only = true) {
global $wpdb;
- $table = BDM_Database::table('retail_sites');
+ $table = Umbra_Database::table('retail_sites');
$where = $active_only ? 'is_active = 1' : '1=1';
@@ -37,7 +37,7 @@ class BDM_Retail_Sites {
public static function get_site($site_id) {
global $wpdb;
- $table = BDM_Database::table('retail_sites');
+ $table = Umbra_Database::table('retail_sites');
return $wpdb->get_row(
$wpdb->prepare(
@@ -53,7 +53,7 @@ class BDM_Retail_Sites {
public static function create_site($data) {
global $wpdb;
- $table = BDM_Database::table('retail_sites');
+ $table = Umbra_Database::table('retail_sites');
$result = $wpdb->insert($table, [
'name' => sanitize_text_field($data['name']),
@@ -74,7 +74,7 @@ class BDM_Retail_Sites {
public static function update_site($site_id, $data) {
global $wpdb;
- $table = BDM_Database::table('retail_sites');
+ $table = Umbra_Database::table('retail_sites');
$update_data = [];
@@ -126,8 +126,8 @@ class BDM_Retail_Sites {
// Only allow deleting custom sites
$site = self::get_site($site_id);
if ($site && $site->is_custom) {
- $wpdb->delete(BDM_Database::table('retail_links'), ['site_id' => $site_id], ['%d']);
- return $wpdb->delete(BDM_Database::table('retail_sites'), ['id' => $site_id], ['%d']) !== false;
+ $wpdb->delete(Umbra_Database::table('retail_links'), ['site_id' => $site_id], ['%d']);
+ return $wpdb->delete(Umbra_Database::table('retail_sites'), ['id' => $site_id], ['%d']) !== false;
}
return false;
@@ -139,13 +139,13 @@ class BDM_Retail_Sites {
public static function generate_links_for_book($book_id) {
global $wpdb;
- $book = BDM_Book_Manager::get_book($book_id);
+ $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) {
return false;
}
$sites = self::get_sites(true);
- $links_table = BDM_Database::table('retail_links');
+ $links_table = Umbra_Database::table('retail_links');
// Delete existing links
$wpdb->delete($links_table, ['book_id' => $book_id], ['%d']);
@@ -266,7 +266,7 @@ class BDM_Retail_Sites {
public static function update_link($link_id, $url) {
global $wpdb;
- $table = BDM_Database::table('retail_links');
+ $table = Umbra_Database::table('retail_links');
return $wpdb->update(
$table,
@@ -283,7 +283,7 @@ class BDM_Retail_Sites {
public static function create_custom_link($book_id, $site_id, $format, $url) {
global $wpdb;
- $table = BDM_Database::table('retail_links');
+ $table = Umbra_Database::table('retail_links');
$result = $wpdb->insert($table, [
'book_id' => $book_id,
diff --git a/bd-bookshelf-plugin/includes/class-bdm-seo.php b/umbra-bookshelf/includes/books/class-umbra-seo.php
similarity index 95%
rename from bd-bookshelf-plugin/includes/class-bdm-seo.php
rename to umbra-bookshelf/includes/books/class-umbra-seo.php
index aaf1592..2612bc1 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-seo.php
+++ b/umbra-bookshelf/includes/books/class-umbra-seo.php
@@ -5,14 +5,14 @@
/**
* SEO Metadata Manager class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_SEO {
+class Umbra_SEO {
/**
* Constructor
@@ -32,7 +32,7 @@ class BDM_SEO {
public static function get_book_seo($book_id) {
global $wpdb;
- $table = BDM_Database::table('book_seo');
+ $table = Umbra_Database::table('book_seo');
$seo = $wpdb->get_row(
$wpdb->prepare(
@@ -50,7 +50,7 @@ class BDM_SEO {
public static function update_book_seo($book_id, $data) {
global $wpdb;
- $table = BDM_Database::table('book_seo');
+ $table = Umbra_Database::table('book_seo');
$existing = self::get_book_seo($book_id);
@@ -85,7 +85,7 @@ class BDM_SEO {
public static function delete_book_seo($book_id) {
global $wpdb;
- $table = BDM_Database::table('book_seo');
+ $table = Umbra_Database::table('book_seo');
return $wpdb->delete($table, ['book_id' => $book_id], ['%d']) !== false;
}
@@ -138,7 +138,7 @@ class BDM_SEO {
}
$seo = self::get_book_seo($book_id);
- $book = BDM_Book_Manager::get_book($book_id);
+ $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) {
return;
@@ -206,7 +206,7 @@ class BDM_SEO {
return $entry;
}
- $book = BDM_Book_Manager::get_book($book_id);
+ $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) {
return $entry;
diff --git a/bd-bookshelf-plugin/includes/class-bdm-series.php b/umbra-bookshelf/includes/books/class-umbra-series.php
similarity index 88%
rename from bd-bookshelf-plugin/includes/class-bdm-series.php
rename to umbra-bookshelf/includes/books/class-umbra-series.php
index 32b591d..ccc5192 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-series.php
+++ b/umbra-bookshelf/includes/books/class-umbra-series.php
@@ -5,14 +5,14 @@
/**
* Series Management class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Series {
+class Umbra_Series {
/**
* Get all series
@@ -20,7 +20,7 @@ class BDM_Series {
public static function get_series($args = []) {
global $wpdb;
- $table = BDM_Database::table('series');
+ $table = Umbra_Database::table('series');
$defaults = [
'number' => 20,
@@ -48,7 +48,7 @@ class BDM_Series {
public static function get_series_by_id($series_id) {
global $wpdb;
- $table = BDM_Database::table('series');
+ $table = Umbra_Database::table('series');
return $wpdb->get_row(
$wpdb->prepare(
@@ -64,7 +64,7 @@ class BDM_Series {
public static function create_series($data) {
global $wpdb;
- $table = BDM_Database::table('series');
+ $table = Umbra_Database::table('series');
$result = $wpdb->insert($table, [
'name' => sanitize_text_field($data['name']),
@@ -81,7 +81,7 @@ class BDM_Series {
public static function update_series($series_id, $data) {
global $wpdb;
- $table = BDM_Database::table('series');
+ $table = Umbra_Database::table('series');
$update_data = [];
@@ -119,7 +119,7 @@ class BDM_Series {
global $wpdb;
// Unlink books from series
- $books_table = BDM_Database::table('books');
+ $books_table = Umbra_Database::table('books');
$wpdb->update(
$books_table,
['series_id' => 0],
@@ -129,7 +129,7 @@ class BDM_Series {
);
// Delete series
- $table = BDM_Database::table('series');
+ $table = Umbra_Database::table('series');
return $wpdb->delete($table, ['id' => $series_id], ['%d']) !== false;
}
@@ -139,7 +139,7 @@ class BDM_Series {
public static function count_series() {
global $wpdb;
- $table = BDM_Database::table('series');
+ $table = Umbra_Database::table('series');
return $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
}
diff --git a/bd-bookshelf-plugin/includes/class-bdm-shortcodes.php b/umbra-bookshelf/includes/books/class-umbra-shortcodes.php
similarity index 91%
rename from bd-bookshelf-plugin/includes/class-bdm-shortcodes.php
rename to umbra-bookshelf/includes/books/class-umbra-shortcodes.php
index 31a9993..430d374 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-shortcodes.php
+++ b/umbra-bookshelf/includes/books/class-umbra-shortcodes.php
@@ -5,14 +5,14 @@
/**
* Shortcodes Manager class
*
- * @package BD_Bookshelf
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
-class BDM_Shortcodes {
+class Umbra_Shortcodes {
/**
* Constructor
@@ -75,7 +75,7 @@ class BDM_Shortcodes {
return '';
}
- $links = BDM_Book_Manager::get_book_retail_links($book_id, $format);
+ $links = Umbra_Book_Manager::get_book_retail_links($book_id, $format);
if (empty($links)) {
return '';
}
@@ -136,12 +136,12 @@ class BDM_Shortcodes {
return '';
}
- $book = BDM_Book_Manager::get_book_by_title(sanitize_text_field($atts['title']));
+ $book = Umbra_Book_Manager::get_book_by_title(sanitize_text_field($atts['title']));
if (!$book) {
return '';
}
- $rating = BDM_Book_Manager::get_rating($book->id);
+ $rating = Umbra_Book_Manager::get_rating($book->id);
$format = in_array($atts['format'], ['stars', 'numeric', 'both']) ? $atts['format'] : 'both';
$output = '';
@@ -201,7 +201,7 @@ class BDM_Shortcodes {
return;
}
- $table = BDM_Database::table('shortcodes');
+ $table = Umbra_Database::table('shortcodes');
$shortcode = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE slug = %s AND is_active = 1",
@@ -228,7 +228,7 @@ class BDM_Shortcodes {
private function track_click($shortcode_id) {
global $wpdb;
- $table = BDM_Database::table('shortcode_clicks');
+ $table = Umbra_Database::table('shortcode_clicks');
$wpdb->insert($table, [
'shortcode_id' => $shortcode_id,
'ip_address' => $this->get_client_ip(),
@@ -239,7 +239,7 @@ class BDM_Shortcodes {
// Increment click count
$wpdb->query(
$wpdb->prepare(
- "UPDATE " . BDM_Database::table('shortcodes') . " SET click_count = click_count + 1 WHERE id = %d",
+ "UPDATE " . Umbra_Database::table('shortcodes') . " SET click_count = click_count + 1 WHERE id = %d",
$shortcode_id
)
);
@@ -268,7 +268,7 @@ class BDM_Shortcodes {
public static function create_shortcode($slug, $target_url, $redirect_type = '301', $description = '') {
global $wpdb;
- $table = BDM_Database::table('shortcodes');
+ $table = Umbra_Database::table('shortcodes');
$result = $wpdb->insert($table, [
'slug' => sanitize_title($slug),
@@ -287,7 +287,7 @@ class BDM_Shortcodes {
public static function update_shortcode($shortcode_id, $data) {
global $wpdb;
- $table = BDM_Database::table('shortcodes');
+ $table = Umbra_Database::table('shortcodes');
$update_data = [];
@@ -332,7 +332,7 @@ class BDM_Shortcodes {
public static function delete_shortcode($shortcode_id) {
global $wpdb;
- $wpdb->delete(BDM_Database::table('shortcode_clicks'), ['shortcode_id' => $shortcode_id], ['%d']);
- return $wpdb->delete(BDM_Database::table('shortcodes'), ['id' => $shortcode_id], ['%d']) !== false;
+ $wpdb->delete(Umbra_Database::table('shortcode_clicks'), ['shortcode_id' => $shortcode_id], ['%d']);
+ return $wpdb->delete(Umbra_Database::table('shortcodes'), ['id' => $shortcode_id], ['%d']) !== false;
}
}
diff --git a/bd-bookshelf-plugin/includes/class-bdm-theme-compatibility.php b/umbra-bookshelf/includes/books/class-umbra-theme-compatibility.php
similarity index 76%
rename from bd-bookshelf-plugin/includes/class-bdm-theme-compatibility.php
rename to umbra-bookshelf/includes/books/class-umbra-theme-compatibility.php
index 7eff1c2..8b83cdf 100644
--- a/bd-bookshelf-plugin/includes/class-bdm-theme-compatibility.php
+++ b/umbra-bookshelf/includes/books/class-umbra-theme-compatibility.php
@@ -5,10 +5,19 @@
/**
* Theme Compatibility Layer
*
- * Provides compatibility with BD BookHelper plugin interface
- * so the BD Bookshelf theme works with our new plugin.
+ * Provides the static helper interface the BD Bookshelf theme calls (book
+ * covers, pen names, ratings, retailer links, series info, ...), backed by the
+ * catalogue module.
*
- * @package BD_Bookshelf
+ * This class used to be declared as BD_BookShelfHelper, which collided with
+ * the library module's own BD_BookShelfHelper class. That collision was latent
+ * while the two were separate plugins -- it only fired when both were active --
+ * but became an unconditional "Cannot declare class" fatal error once they
+ * were merged into a single plugin. It now has its own name, and the
+ * class_alias() at the bottom keeps the legacy name working for the
+ * unmodified theme.
+ *
+ * @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
@@ -16,10 +25,9 @@ if (!defined('ABSPATH')) {
}
/**
- * BD_BookShelfHelper class for theme compatibility
- * Maps theme expectations to our new plugin methods
+ * Static helper API consumed by the theme.
*/
-class BD_BookShelfHelper {
+class Umbra_Theme_Compatibility {
/**
* Get book cover URL
@@ -77,7 +85,7 @@ class BD_BookShelfHelper {
return [];
}
- $links = BDM_Book_Manager::get_book_retail_links($book->id, $format);
+ $links = Umbra_Book_Manager::get_book_retail_links($book->id, $format);
$retailers = [];
foreach ($links as $link) {
@@ -102,7 +110,7 @@ class BD_BookShelfHelper {
* Get series book count
*/
public static function get_series_book_count($series_id) {
- $books = BDM_Book_Manager::get_books_by_series($series_id);
+ $books = Umbra_Book_Manager::get_books_by_series($series_id);
return count($books);
}
@@ -110,7 +118,7 @@ class BD_BookShelfHelper {
* Get series info
*/
public static function get_series_info($series_id) {
- $series = BDM_Series::get_series_by_id($series_id);
+ $series = Umbra_Series::get_series_by_id($series_id);
if ($series) {
return [
'name' => $series->name,
@@ -189,7 +197,7 @@ class BD_BookShelfHelper {
global $wpdb;
// Check if post_id is actually a book ID from our database
- $book = BDM_Book_Manager::get_book($post_id);
+ $book = Umbra_Book_Manager::get_book($post_id);
if ($book) {
return $book;
}
@@ -198,13 +206,13 @@ class BD_BookShelfHelper {
$book_id = get_post_meta($post_id, '_bd_bookshelf_book_id', true);
if ($book_id) {
- return BDM_Book_Manager::get_book($book_id);
+ return Umbra_Book_Manager::get_book($book_id);
}
// Try to find by title
$post = get_post($post_id);
if ($post) {
- $book = BDM_Book_Manager::get_book_by_title($post->post_title);
+ $book = Umbra_Book_Manager::get_book_by_title($post->post_title);
if ($book) {
// Link post to book
update_post_meta($post_id, '_bd_bookshelf_book_id', $book->id);
@@ -215,3 +223,14 @@ class BD_BookShelfHelper {
return null;
}
}
+
+/**
+ * Legacy alias.
+ *
+ * The BD Bookshelf theme (unchanged, and out of scope for this consolidation)
+ * still calls BD_BookShelfHelper:: statically. Keep that working. Guarded
+ * because a theme or add-on may legitimately have declared the name already.
+ */
+if (!class_exists('BD_BookShelfHelper', false)) {
+ class_alias('Umbra_Theme_Compatibility', 'BD_BookShelfHelper');
+}
diff --git a/bd-simpleemail/inc/class-email-admin.php b/umbra-bookshelf/includes/email/class-umbra-email-admin.php
similarity index 92%
rename from bd-simpleemail/inc/class-email-admin.php
rename to umbra-bookshelf/includes/email/class-umbra-email-admin.php
index d9e5723..f08c752 100644
--- a/bd-simpleemail/inc/class-email-admin.php
+++ b/umbra-bookshelf/includes/email/class-umbra-email-admin.php
@@ -5,20 +5,20 @@
/**
* Email Admin Class
*
- * @package BD_SimpleEmail
+ * @package Umbra_Simple_Email
*/
if (!defined('ABSPATH')) {
exit;
}
-class BD_SimpleEmail_Admin {
+class Umbra_Simple_Email_Admin {
private static $instance = null;
/**
* Get instance
*
- * @return BD_SimpleEmail_Admin
+ * @return Umbra_Simple_Email_Admin
*/
public static function instance() {
if (null === self::$instance) {
@@ -40,19 +40,23 @@ class BD_SimpleEmail_Admin {
* @param string $hook Current admin page.
*/
public function enqueue_assets($hook) {
- if (strpos($hook, 'bd-simpleemail') !== false || strpos($hook, 'bd-email') !== false) {
+ // Matches the submenu slugs registered by Umbra_Simple_Email::add_menu_pages().
+ if (strpos($hook, 'umbra-bookshelf-subscribers') !== false
+ || strpos($hook, 'umbra-bookshelf-newsletter') !== false
+ || strpos($hook, 'umbra-bookshelf-email-settings') !== false) {
+
wp_enqueue_style(
- 'bd-email-admin',
- BD_EMAIL_URI . 'assets/css/admin.css',
+ 'umbra-bookshelf-email-admin',
+ UMBRA_BOOKSHELF_URL . 'email/assets/css/admin.css',
array(),
- BD_EMAIL_VERSION
+ UMBRA_BOOKSHELF_EMAIL_VERSION
);
wp_enqueue_script(
- 'bd-email-admin',
- BD_EMAIL_URI . 'assets/js/admin.js',
+ 'umbra-bookshelf-email-admin',
+ UMBRA_BOOKSHELF_URL . 'email/assets/js/admin.js',
array('jquery'),
- BD_EMAIL_VERSION,
+ UMBRA_BOOKSHELF_EMAIL_VERSION,
true
);
}
@@ -143,7 +147,7 @@ class BD_SimpleEmail_Admin {
}
if ($content) {
- $result = BD_Email_Sender::send_newsletter($subject, $content);
+ $result = Umbra_Email_Sender::send_newsletter($subject, $content);
if ($result['success']) {
self::log_newsletter($subject, $content, $result['count']);
@@ -163,7 +167,7 @@ class BD_SimpleEmail_Admin {
}
}
- $settings = BD_SimpleEmail::get_settings();
+ $settings = Umbra_Simple_Email::get_settings();
$has_ai = !empty($settings['gemini_api_key']);
echo '
';
@@ -231,7 +235,7 @@ class BD_SimpleEmail_Admin {
update_option('bdm_reply_to', sanitize_email($_POST['reply_to'] ?? ''));
if (!empty($_POST['gmail_password'])) {
- update_option('bdm_gmail_password', BD_Email_Sender::encrypt_password($_POST['gmail_password']));
+ update_option('bdm_gmail_password', Umbra_Email_Sender::encrypt_password($_POST['gmail_password']));
}
if (!empty($_POST['gemini_api_key'])) {
@@ -241,8 +245,8 @@ class BD_SimpleEmail_Admin {
echo '
';
foreach ($languages as $code => $name) {
echo '
';
@@ -304,10 +316,10 @@ class BD_BookShelfHelper_Admin {
* @param WP_Post $post Post object.
*/
public function book_seo_meta_box($post) {
- echo '
';
+ echo '
';
echo '
';
- echo '
';
+ echo '
';
echo '
';
}
@@ -316,7 +328,7 @@ class BD_BookShelfHelper_Admin {
*/
public function data_entry_page() {
$post_id = isset($_GET['post_id']) ? absint($_GET['post_id']) : 0;
- $languages = BD_BookShelfHelper::get_supported_languages();
+ $languages = Umbra_Bookshelf_Helper::get_supported_languages();
// Get all books for selection
global $wpdb;
@@ -327,9 +339,9 @@ class BD_BookShelfHelper_Admin {
// If no post_id provided and no books exist, show selection
if (!$post_id && empty($books)) {
echo '
';
- echo '
' . esc_html__('Book Data Entry', 'bd-bookshelfhelper') . '
';
- echo '
' . esc_html__('No books found. Please create a book first.', 'bd-bookshelfhelper') . '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('The full title of the book as it appears on the cover.', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('The full title of the book as it appears on the cover.', 'umbra-bookshelf') . '';
echo '
';
echo '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('The author name to display with this book.', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('The author name to display with this book.', 'umbra-bookshelf') . '';
echo '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('Select the series this book belongs to, or leave blank for standalone.', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('Select the series this book belongs to, or leave blank for standalone.', 'umbra-bookshelf') . '';
echo '
';
echo '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('Order within the series (e.g., 1 for Book 1).', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('Order within the series (e.g., 1 for Book 1).', 'umbra-bookshelf') . '';
echo '
';
echo '
'; // End Series
// Section 4: Short Link
- $this->render_section_header('shortlink', __('Short Link', 'bd-bookshelfhelper'), false);
+ $this->render_section_header('shortlink', __('Short Link', 'umbra-bookshelf'), false);
echo '
';
echo '
';
- echo '';
+ echo '';
$current_slug = get_post_meta($post_id, 'short_link_slug', true);
if (empty($current_slug)) {
$current_slug = $this->generate_short_slug($post->post_title);
}
echo '' . esc_url(home_url('/')) . '';
echo '';
- echo '' . esc_html__('Auto-generated from title if empty. E.g., "ph" for "Pandemic Hacker"', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('Auto-generated from title if empty. E.g., "ph" for "Pandemic Hacker"', 'umbra-bookshelf') . '';
echo '
';
echo '
'; // End Short Link
// Section 5: Language Content (tabs for each language)
- $this->render_section_header('content', __('Content (by Language)', 'bd-bookshelfhelper'), true);
+ $this->render_section_header('content', __('Content (by Language)', 'umbra-bookshelf'), true);
echo '
';
foreach ($languages as $code => $name) {
@@ -509,48 +521,48 @@ class BD_BookShelfHelper_Admin {
echo '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('Title in this language (if different from default).', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('Title in this language (if different from default).', 'umbra-bookshelf') . '';
echo '
';
echo '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('The book description shown on the back cover and product pages.', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('The book description shown on the back cover and product pages.', 'umbra-bookshelf') . '';
echo '
';
echo '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('A short excerpt (50-100 words) for previews and "Look Inside" feature.', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('A short excerpt (50-100 words) for previews and "Look Inside" feature.', 'umbra-bookshelf') . '';
echo '
';
- echo '' . esc_html__('Notable quotes or reviews to display on the book page.', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('Notable quotes or reviews to display on the book page.', 'umbra-bookshelf') . '';
echo '
';
- echo '';
+ echo '';
echo '';
- echo '' . esc_html__('Custom title for search engines (defaults to book title if empty).', 'bd-bookshelfhelper') . '';
+ echo '' . esc_html__('Custom title for search engines (defaults to book title if empty).', 'umbra-bookshelf') . '';
echo '
' . esc_html__('Migrate from BD Books', 'bd-bookshelfhelper') . '
';
+ echo '
' . esc_html__('Migrate from BD Books', 'umbra-bookshelf') . '
';
if (!$table_exists) {
echo '
';
- echo '
' . esc_html__('The old BD Books table was not found. No migration needed - the plugin data does not exist.', 'bd-bookshelfhelper') . '
';
+ echo '
' . esc_html__('The old BD Books table was not found. No migration needed - the plugin data does not exist.', 'umbra-bookshelf') . '
';
echo '
';
- echo '
' . esc_html__('This tool migrates books from the original "BD Books" (BD Book Listing) plugin to the new BookHelper system using custom post types.', 'bd-bookshelfhelper') . '
';
+ echo '
' . esc_html__('This tool migrates books from the original "BD Books" (BD Book Listing) plugin to the new BookHelper system using custom post types.', 'umbra-bookshelf') . '