Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

278
native/wordpress/README.md Normal file
View file

@ -0,0 +1,278 @@
# 🔌 WordPress Native Applications
This directory contains native WordPress applications (plugins and themes) for the Maple Open Technologies ecosystem.
## 🗂️ Directory Structure
```
native/wordpress/
└── maplepress-plugin/ # MaplePress cloud services plugin
```
## 🚀 MaplePress Plugin
The MaplePress plugin connects your WordPress site to the MaplePress cloud platform, providing cloud-powered services that offload computationally intensive tasks to remote infrastructure. Currently features advanced search capabilities, with more services coming soon (uploads, metrics, analytics, etc.).
### Features
- **Cloud-powered search** - Offsite search processing with advanced indexing
- **Automatic content indexing** - Your content is automatically synced
- **Real-time synchronization** - Changes are reflected immediately
- **API key authentication** - Secure connection to MaplePress backend
- **WordPress admin settings** - Easy configuration and monitoring
- **Extensible platform** - Ready for additional cloud services as they become available
### Development Workflow
#### Local Development with Docker
The plugin is automatically mounted into the local WordPress container for development:
```bash
# Start infrastructure (from cloud/infrastructure/development)
cd ../../cloud/infrastructure/development
task dev:start
# WordPress will be available at http://localhost:8081
# Plugin is auto-mounted at /wp-content/plugins/maplepress-plugin
```
#### First Time Setup
Configure the plugin for local development:
```bash
cd maplepress-plugin
# Set up local development environment
task dev:setup
# This will:
# 1. Create wp-config.local.php with API URL = http://localhost:8000
# 2. Inject loader into WordPress wp-config.php
# 3. Pre-configure API URL for local development
# Now activate the plugin in WordPress:
# - Go to http://localhost:8081/wp-admin/plugins.php
# - Activate "MaplePress" plugin
# - Go to Settings → MaplePress
# - API URL will be pre-filled with http://localhost:8000
# - Enter your API key and save
```
**Why this approach?**
- Production plugin distributed via WordPress.org has **empty API URL** (users must configure)
- Local development needs **http://localhost:8000** pre-configured for convenience
- Can't use `.env` files (not part of plugin distribution)
- Solution: Local-only config file (`wp-config.local.php`) that is git-ignored and excluded from builds
#### Plugin Development Commands
```bash
cd maplepress-plugin
# Set up local development (first time)
task dev:setup
# Reset to production configuration (removes local dev settings)
task dev:reset
# Sync plugin to WordPress container (manual sync)
task sync
# Watch for changes and auto-sync
task watch
# View WordPress debug logs
task logs
# Open shell in WordPress container
task shell
# Run PHP CodeSniffer
task lint
# Auto-fix coding standards issues
task lint:fix
# Run tests
task test
# Build distribution ZIP
task build
# Clean build artifacts
task clean
# Install Composer dependencies
task install
```
### Initial Setup
1. **Start WordPress:**
```bash
cd cloud/infrastructure/development
task dev:start
```
2. **Complete WordPress Installation:**
- Visit http://localhost:8081
- Complete the WordPress setup wizard
- Create admin account
3. **Activate Plugin:**
- Go to Plugins → Installed Plugins
- Activate "MaplePress"
4. **Configure Plugin:**
- Go to Settings → MaplePress
- Enter API URL: `http://maplepress-backend:8000` (or your backend URL)
- Enter your API key from the MaplePress dashboard
- Enable MaplePress and save settings
### Plugin Structure
```
maplepress-plugin/
├── maplepress-plugin.php # Main plugin file
├── readme.txt # WordPress.org readme
├── composer.json # PHP dependencies
├── Taskfile.yml # Development tasks
├── includes/ # Core plugin logic
│ ├── class-maplepress.php # Main plugin class
│ ├── class-maplepress-loader.php # Hook loader
│ ├── class-maplepress-activator.php # Activation logic
│ ├── class-maplepress-deactivator.php # Deactivation logic
│ ├── class-maplepress-admin.php # Admin functionality
│ ├── class-maplepress-public.php # Public functionality
│ ├── class-maplepress-api-client.php # API client
│ └── admin-settings-display.php # Settings page template
├── assets/ # Frontend assets
│ ├── css/
│ │ ├── maplepress-admin.css
│ │ └── maplepress-public.css
│ └── js/
│ ├── maplepress-admin.js
│ └── maplepress-public.js
├── languages/ # Translation files
└── tests/ # PHPUnit tests
```
### Publishing to WordPress.org
1. **Prepare for Release:**
```bash
# Update version in:
# - maplepress-plugin.php (header and constant)
# - readme.txt (Stable tag)
# Build distribution package
task build
```
2. **Create Release:**
- Distribution ZIP will be in `dist/maplepress-plugin.zip`
- Test the ZIP in a fresh WordPress installation
3. **Submit to WordPress.org:**
- Create account at https://wordpress.org/plugins/developers/
- Submit plugin for review
- Follow WordPress plugin guidelines
### API Integration
The plugin communicates with the MaplePress backend using the API client:
```php
// Example: Verify API connection
$api_client = new MaplePress_API_Client( $api_url, $api_key );
$status = $api_client->verify_connection();
// Example: Index a post
$api_client->index_post( $post_id );
// Example: Search
$results = $api_client->search( 'search query' );
```
### WordPress Coding Standards
The plugin follows WordPress Coding Standards:
```bash
# Check coding standards
task lint
# Auto-fix issues
task lint:fix
```
### Testing
```bash
# Run PHPUnit tests
task test
# Test in local WordPress
# 1. Make changes to plugin code
# 2. Refresh WordPress admin to see changes
# 3. Check debug logs: task logs
```
### Debugging
WordPress debug mode is enabled in the development environment:
```bash
# View debug logs
task logs
# Or directly:
docker exec -it maple-wordpress-dev tail -f /var/www/html/wp-content/debug.log
```
### Environment Variables
The plugin uses WordPress options for configuration:
- `maplepress_settings['api_url']` - MaplePress backend URL
- `maplepress_settings['api_key']` - Site API key
- `maplepress_settings['site_id']` - Site ID (auto-populated)
- `maplepress_settings['enabled']` - Enable/disable plugin
### Docker Volume Mount
The plugin is mounted as a read-only volume in docker-compose.dev.yml:
```yaml
volumes:
- ../../native/wordpress/maplepress-plugin:/var/www/html/wp-content/plugins/maplepress-plugin:ro
```
This allows live editing without rebuilding the container.
## Adding New WordPress Plugins
To add additional WordPress plugins to the monorepo:
1. Create new directory: `native/wordpress/new-plugin-name/`
2. Follow WordPress plugin structure
3. Add Taskfile.yml for development workflow
4. Mount in docker-compose.dev.yml if needed for local development
5. Update this README
## 📚 Resources
- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/)
- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/)
- [Plugin API Reference](https://developer.wordpress.org/reference/)
- [WordPress.org Plugin Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)
## Contributing
Found a bug? Want a feature to improve the WordPress plugins? Please create an [issue](https://codeberg.org/mapleopentech/monorepo/issues/new).
## License
This application is licensed under the [**GNU Affero General Public License v3.0**](https://opensource.org/license/agpl-v3). See [LICENSE](../../LICENSE) for more information.

View file

@ -0,0 +1,26 @@
# Local development configuration
wp-config.local.php
# Composer
/vendor/
composer.lock
# Node
/node_modules/
package-lock.json
# Build artifacts
/dist/
# IDE
.idea/
.vscode/
*.sublime-project
*.sublime-workspace
# OS
.DS_Store
Thumbs.db
# WordPress
*.log

View file

@ -0,0 +1,104 @@
# MaplePress Plugin Changelog
## [1.0.0] - 2024-10-27
### Added - API Key Authentication Flow
#### Activation Experience
- Plugin now redirects to settings page upon activation
- Setup wizard shows welcome message with step-by-step instructions
- Admin notice appears on all pages until API key is configured
- Links to external MaplePress dashboard for account creation
#### Settings Page Enhancements
- **Welcome Screen**: First-time users see detailed setup instructions
- **External Dashboard Links**:
- Sign Up button: `https://getmaplepress.com/register`
- Login button: `https://getmaplepress.com/login`
- Dashboard link: `https://getmaplepress.com/dashboard`
- **API Key Validation**:
- Validates key with backend on save
- Shows clear success/error messages
- Displays site details after successful connection
- **Connection Status Display**:
- Site ID
- Tenant ID
- Domain
- Plan tier
- Active/Inactive status indicator
#### API Integration
- Enhanced API client error handling
- Stores site details from `/api/v1/plugin/status` response:
- `site_id`
- `tenant_id`
- `domain`
- `plan_tier`
- `is_verified`
- Validates API key on every settings save
- Clear error messages for connection failures
#### Admin Experience
- **Admin Notice**: Persistent warning banner when setup is incomplete
- **Auto-redirect**: Redirects to settings page after activation
- **Smart Display**: Notices don't show on settings page itself
- **Status Indicators**: Visual feedback (✓/✗ and colored dots)
### Technical Changes
#### New Fields in Settings
- `tenant_id` - Tenant UUID from API
- `domain` - Site domain from API
- `plan_tier` - User's subscription plan
- `is_verified` - Boolean flag for valid API key
- `needs_setup` - Boolean flag to trigger setup notices
#### Modified Files
- `includes/class-maplepress-activator.php`
- Added new settings fields
- Default API URL set to `http://localhost:8000`
- Added activation redirect transient
- `includes/class-maplepress-admin.php`
- Added `display_admin_notices()` method
- Added `activation_redirect()` method
- Enhanced `validate_settings()` with site details storage
- Better error handling and user feedback
- `includes/admin-settings-display.php`
- Complete redesign of settings page
- Conditional display based on connection status
- Welcome wizard for first-time setup
- Site details table for connected sites
### User Workflow
1. **Install & Activate**
- Plugin activates → redirects to settings page
- Sees welcome message with instructions
2. **Get API Key**
- Clicks "Sign Up at MaplePress.io"
- Creates account and site in external dashboard
- Copies API key (shown only once)
3. **Configure Plugin**
- Pastes API key in settings
- Clicks "Save Settings & Verify Connection"
- Plugin validates key with backend
- Success: Shows site details and status
- Failure: Shows error message
4. **Ongoing Use**
- If not configured: Admin notice appears on all pages
- If configured: Status shows in settings page
- Can manage sites from external dashboard
### API Endpoints Used
- `GET /api/v1/plugin/status` - Verify API key and get site details
### Security
- API keys stored in WordPress options table
- All user input sanitized and escaped
- API communication over HTTPS (production)
- WordPress nonces for form submissions

View file

@ -0,0 +1,745 @@
# MaplePress Plugin - Getting Started Guide
This guide will walk you through setting up the MaplePress plugin for both local development and production release. No prior WordPress experience required!
---
## Table of Contents
1. [What is WordPress?](#what-is-wordpress)
2. [Local Development Setup](#local-development-setup)
3. [Production Release Workflow](#production-release-workflow)
4. [Common Tasks](#common-tasks)
5. [Troubleshooting](#troubleshooting)
---
## What is WordPress?
**WordPress** is a content management system (CMS) written in PHP. It powers over 40% of websites on the internet.
**WordPress Plugin** = A piece of software that adds specific functionality to a WordPress site. Our MaplePress plugin adds cloud-powered search functionality.
**Key WordPress Concepts:**
- **wp-admin** = WordPress admin dashboard (where you configure plugins)
- **wp-config.php** = WordPress configuration file (like a .env file for WordPress)
- **wp-content/plugins/** = Directory where all plugins are stored
- **Activation** = Turning on a plugin in WordPress
- **Settings Page** = Admin interface where users configure your plugin
---
## Local Development Setup
### Prerequisites
Make sure you have:
- Docker running
- Task (taskfile.dev) installed
- MaplePress backend running
### Step 1: Start WordPress
```bash
# From the infrastructure directory
cd cloud/infrastructure/development
# Start all services (includes WordPress)
task dev:start
# OR if using docker-compose directly:
docker-compose -f docker-compose.dev.yml up -d
```
**What this does:**
- Starts MariaDB (WordPress database) on port 3306
- Starts WordPress on port 8081
- Starts Cassandra cluster (3 nodes) for backend
- Starts Redis for caching
- Starts Meilisearch for search indexing
- Starts SeaweedFS for object storage
- Mounts your plugin directory into WordPress
**Note:** This starts the entire infrastructure, not just WordPress. The `task dev:start` command will:
1. Start all Docker containers
2. Wait for services to be healthy (this can take 1-2 minutes)
3. Initialize Cassandra keyspaces
4. Show you the status of all running services
**Access WordPress:**
- Frontend: http://localhost:8081
- Admin: http://localhost:8081/wp-admin
**Other Services (for backend development):**
- Backend API: http://localhost:8000 (when you start maplepress-backend)
- Meilisearch: http://localhost:7700
- SeaweedFS: http://localhost:9333
### Step 2: Initial WordPress Setup (First Time Only)
If this is your first time, WordPress will ask you to set it up:
1. Open http://localhost:8081 in your browser
2. Select language (English)
3. Click "Let's go!"
4. Database settings are already configured (from docker-compose.dev.yml):
- Database Name: `wordpress`
- Username: `wordpress`
- Password: `wordpress`
- Database Host: `mariadb` (the container name)
- Table Prefix: `wp_` (leave default)
5. Click "Submit" then "Run the installation"
6. Create admin user:
- Site Title: "MaplePress Development"
- Username: `admin` (or your choice)
- Password: (choose a strong password)
- Email: your email
7. Click "Install WordPress"
8. Login with your admin credentials
**You only need to do this once.** WordPress saves everything in the database.
### Step 3: Configure Plugin for Local Development
```bash
# From the plugin directory
cd native/wordpress/maplepress-plugin
# Run the setup command
task dev:setup
```
**What this does:**
1. Creates `wp-config.local.php` with `MAPLEPRESS_API_URL` set to `http://localhost:8000`
2. Modifies WordPress `wp-config.php` to load your local config
3. Pre-configures the plugin to connect to your local backend
**Output:**
```
Created wp-config.local.php
Added loader to wp-config.php
Local development environment configured
API URL pre-configured to http://localhost:8000
Go to Settings > MaplePress to enter your API key
```
### Step 4: Activate the Plugin in WordPress
1. Go to http://localhost:8081/wp-admin/plugins.php
2. Find "MaplePress" in the plugin list
3. Click "Activate"
4. You'll be redirected to Settings → MaplePress
### Step 5: Enter Your API Key
1. You should now be on the MaplePress settings page
2. You'll see:
- **API URL** - Pre-filled with `http://localhost:8000`
- **API Key** - Empty (you need to enter this)
3. Get your API key:
- If you don't have one yet, follow the backend setup guide:
- See `cloud/maplepress-backend/GETTING-STARTED.md` → "Create Test Data"
- Or use the quick commands below:
```bash
# Register a user and create a site (returns API key)
# See backend GETTING-STARTED.md for detailed instructions
curl -X POST http://localhost:8000/api/v1/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestPassword123!","name":"Test User","tenant_name":"Test Org","tenant_slug":"test-org"}'
# Then create a site (save the API key from response)
curl -X POST http://localhost:8000/api/v1/sites \
-H "Authorization: JWT YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"domain":"localhost:8081","site_url":"http://localhost:8081","plan_tier":"free"}'
```
4. Paste the API key
5. Click "Save Settings & Verify Connection"
**If successful:**
- Green message: "✓ API connection verified successfully!"
- You'll see your site details (Site ID, Tenant ID, Domain, Plan)
- Status shows "Active"
**If it fails:**
- Red error message explaining what went wrong
- Check that backend is running: `curl http://localhost:8000/health`
- Check API key is valid
- See backend docs: `cloud/maplepress-backend/GETTING-STARTED.md`
### Step 6: Start Developing
Your local development environment is now ready!
**Making code changes:**
Option 1: Auto-sync (Recommended)
```bash
cd native/wordpress/maplepress-plugin
# Watch for changes and auto-sync
task watch
```
This will automatically copy changes to WordPress whenever you save a file.
Option 2: Manual sync
```bash
# After making changes, sync manually
task sync
```
Option 3: Direct editing (Advanced)
- Plugin is mounted as a volume, so changes are usually reflected immediately
- Just refresh your browser after editing PHP files
- Some changes require plugin reactivation
**Viewing logs:**
```bash
# See WordPress debug logs
task logs
# Access WordPress container shell
task shell
```
---
## Production Release Workflow
### Understanding Production vs Development
**Development:**
- API URL: `http://localhost:8000` (your local backend)
- API Key: Your test API key
- Config file: `wp-config.local.php` (git-ignored)
**Production:**
- API URL: `https://getmaplepress.ca` (production backend)
- API Key: User enters their own API key from dashboard
- Config file: None (users configure via settings)
### Step 1: Prepare for Release
**Clean up local config:**
```bash
cd native/wordpress/maplepress-plugin
# Remove local development settings
task dev:reset
```
This removes:
- `wp-config.local.php` (local config file)
- Loader code from WordPress `wp-config.php`
**Verify production behavior:**
1. Deactivate the plugin in WordPress
2. Delete plugin options: Settings → MaplePress → Delete (if there's an option)
3. Reactivate the plugin
4. Go to Settings → MaplePress
5. Both API URL and API Key should be **empty**
This is what users will see when they install your plugin.
### Step 2: Build Production Zip
```bash
cd native/wordpress/maplepress-plugin
# Build the distribution package
task build
```
**What this does:**
1. Creates `dist/` directory
2. Copies all plugin files EXCEPT:
- `wp-config.local.php` (local dev config)
- `.git/` (version control)
- `node_modules/` (dependencies)
- `Taskfile.yml` (development tool)
- `composer.json/lock` (PHP dependencies)
- `.gitignore` (git config)
- `dist/` (avoid nested builds)
3. Creates `dist/maplepress-plugin.zip`
**Output:**
```
Plugin built at dist/maplepress-plugin.zip
Local dev files excluded from build
```
### Step 3: Test Production Build Locally (Optional but Recommended)
Before releasing, test the production build:
```bash
# Extract and verify contents
cd dist
unzip -l maplepress-plugin.zip
# Should NOT contain:
# - wp-config.local.php
# - Taskfile.yml
# - composer.json
# - .gitignore
# - .git/
# Should contain:
# - maplepress-plugin.php (main file)
# - includes/ (PHP classes)
# - assets/ (CSS/JS)
# - readme.txt (WordPress.org readme)
# - CHANGELOG.md, TESTING.md, etc.
```
**Test installation:**
1. Copy the zip to a clean WordPress install (or delete current plugin)
2. Go to Plugins → Add New → Upload Plugin
3. Choose `dist/maplepress-plugin.zip`
4. Click "Install Now"
5. Activate the plugin
6. Verify settings page shows:
- Empty API URL field ✓
- Empty API Key field ✓
- Placeholder: `https://getmaplepress.ca`
### Step 4: Release to WordPress.org
**WordPress Plugin Directory Submission:**
1. **Create WordPress.org Account**
- Go to https://wordpress.org/support/register.php
- Create an account (you'll need this to submit plugins)
2. **Submit Plugin for Review**
- Go to https://wordpress.org/plugins/developers/add/
- Upload your `dist/maplepress-plugin.zip`
- Wait for manual review (can take 2-14 days)
- You'll receive an email with SVN repository access
3. **Using SVN (Subversion)**
WordPress.org uses SVN, not Git:
```bash
# Checkout your plugin's SVN repository
# (URL provided in approval email)
svn co https://plugins.svn.wordpress.org/maplepress YOUR_LOCAL_DIR
cd YOUR_LOCAL_DIR
# Directory structure:
# /trunk/ - Development version
# /tags/ - Released versions (1.0.0, 1.0.1, etc.)
# /assets/ - Screenshots, banners, icons
```
4. **Initial Release (Version 1.0.0)**
```bash
# Extract your plugin to /trunk/
cd trunk
unzip ../../dist/maplepress-plugin.zip
mv maplepress-plugin/* .
rmdir maplepress-plugin
# Add files to SVN
svn add --force * --auto-props --parents --depth infinity -q
# Commit to trunk
svn ci -m "Initial release v1.0.0"
# Create a tag for this version
cd ..
svn cp trunk tags/1.0.0
svn ci -m "Tagging version 1.0.0"
```
5. **Add Screenshots and Assets**
```bash
# Add to /assets/ directory
cd assets
# Required files:
# - icon-128x128.png (plugin icon)
# - icon-256x256.png (retina icon)
# - screenshot-1.png (settings page)
# - screenshot-2.png (search widget)
# - banner-772x250.png (plugin page banner)
# - banner-1544x500.png (retina banner)
svn add *.png
svn ci -m "Add plugin assets"
```
### Step 5: Version Updates
When releasing a new version:
```bash
# 1. Update version in plugin files
# Edit maplepress-plugin.php:
# Version: 1.0.1
# Edit readme.txt:
# Stable tag: 1.0.1
# 2. Build new release
cd native/wordpress/maplepress-plugin
task build
# 3. Update SVN trunk
cd YOUR_SVN_DIR/trunk
# Replace files with new build
unzip ../../dist/maplepress-plugin.zip
mv maplepress-plugin/* .
# Commit to trunk
svn stat # See what changed
svn ci -m "Update to version 1.0.1"
# 4. Create new tag
cd ..
svn cp trunk tags/1.0.1
svn ci -m "Tagging version 1.0.1"
```
WordPress.org will automatically update users' plugins within 24 hours.
---
## Common Tasks
### Switch Between Local Dev and Production Testing
**Enable local development:**
```bash
task dev:setup
# Restart WordPress
docker-compose -f ../../cloud/infrastructure/development/docker-compose.dev.yml restart wordpress
```
**Test production behavior:**
```bash
task dev:reset
# Deactivate/reactivate plugin in WordPress
```
### View Plugin in WordPress
- **Plugins page:** http://localhost:8081/wp-admin/plugins.php
- **Settings page:** http://localhost:8081/wp-admin/options-general.php?page=maplepress
### Check If Plugin Is Active
```bash
# From container
docker exec maple-wordpress-dev wp plugin list
# Should show:
# name status
# maplepress active
```
### Manually Inspect Database
```bash
# Connect to MariaDB
docker exec -it maple-mariadb-dev mysql -u wordpress -pwordpress wordpress
# Check plugin options
SELECT * FROM wp_options WHERE option_name = 'maplepress_settings';
# Exit
exit
```
### Reset WordPress Completely
```bash
# Stop containers
docker-compose -f docker-compose.dev.yml down
# Delete volumes (WARNING: Deletes all data)
docker volume rm maple-wordpress-dev
docker volume rm maple-mariadb-dev
# Start fresh
docker-compose -f docker-compose.dev.yml up -d
# You'll need to go through WordPress setup again
```
### Check Plugin Files in Container
```bash
# Access container shell
task shell
# Inside container:
cd /var/www/html/wp-content/plugins/maplepress-plugin
ls -la
# Check if wp-config.local.php exists
cat wp-config.local.php # Should show MAPLEPRESS_API_URL
# Check WordPress config has loader
grep -A2 "MaplePress local" /var/www/html/wp-config.php
```
---
## Troubleshooting
### "Task does not exist" Error
**Error:** `task: Task "dev" does not exist`
**Solution:** The correct command is `task dev:start` (not `task dev`)
```bash
cd cloud/infrastructure/development
task dev:start # ✓ Correct
# NOT: task dev # ✗ Wrong
```
**Available infrastructure tasks:**
- `task dev:start` - Start all services
- `task dev:stop` - Stop all services
- `task dev:status` - Show service status
- `task dev:restart` - Restart all services
- `task dev:logs` - View logs
- `task dev:clean` - Remove all data (destructive)
### "Cannot connect to API" Error
**Possible causes:**
1. **Backend not running**
```bash
# Check if backend is up
curl http://localhost:8000/health
# Should return: {"status":"healthy"}
# If not, start backend:
cd cloud/maplepress-backend
task dev
# For detailed setup, see: cloud/maplepress-backend/GETTING-STARTED.md
```
2. **Wrong API URL**
- Check Settings → MaplePress
- For local dev, should be: `http://localhost:8000`
- Not `localhost:8081` (that's WordPress)
- Not `https://` (local dev is http)
3. **Invalid API Key**
- API key must exist in backend database
- Check backend logs for authentication errors
### "Plugin not showing up"
1. **Check plugin directory:**
```bash
docker exec maple-wordpress-dev ls -la /var/www/html/wp-content/plugins/
```
Should see `maplepress-plugin/`
2. **Check main plugin file exists:**
```bash
docker exec maple-wordpress-dev ls -la /var/www/html/wp-content/plugins/maplepress-plugin/maplepress-plugin.php
```
3. **Check plugin header:**
```bash
docker exec maple-wordpress-dev head -n 15 /var/www/html/wp-content/plugins/maplepress-plugin/maplepress-plugin.php
```
Should show "Plugin Name: MaplePress"
4. **Restart WordPress:**
```bash
docker-compose -f docker-compose.dev.yml restart wordpress
```
### "API URL not pre-filled" in Local Dev
1. **Check local config exists:**
```bash
cat wp-config.local.php
```
Should show `define('MAPLEPRESS_API_URL', 'http://localhost:8000');`
2. **Check WordPress config has loader:**
```bash
docker exec maple-wordpress-dev grep -A2 "MaplePress" /var/www/html/wp-config.php
```
Should show loader code
3. **Re-run setup:**
```bash
task dev:reset
task dev:setup
```
4. **Deactivate and reactivate plugin:**
- Go to http://localhost:8081/wp-admin/plugins.php
- Click "Deactivate" under MaplePress
- Click "Activate"
- Check settings page again
### "Changes not showing up"
**For PHP changes:**
1. **Deactivate and reactivate plugin**
- WordPress caches plugin code
2. **Or sync manually:**
```bash
task sync
```
3. **Or restart WordPress:**
```bash
docker-compose -f ../../cloud/infrastructure/development/docker-compose.dev.yml restart wordpress
```
**For CSS/JS changes:**
- Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows)
- Or clear browser cache
### "Permission denied" Errors
```bash
# Fix plugin directory permissions
docker exec maple-wordpress-dev chown -R www-data:www-data /var/www/html/wp-content/plugins/maplepress-plugin
# Restart WordPress
docker-compose -f docker-compose.dev.yml restart wordpress
```
### WordPress Shows "Fatal Error"
**Check PHP error log:**
```bash
docker exec maple-wordpress-dev tail -f /var/www/html/wp-content/debug.log
```
**Enable WordPress debug mode:**
```bash
docker exec maple-wordpress-dev bash -c "
grep -q 'WP_DEBUG' /var/www/html/wp-config.php ||
sed -i \"/That's all, stop editing/i define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\" /var/www/html/wp-config.php
"
```
**Syntax error in PHP:**
- Check PHP syntax: `php -l includes/class-maplepress-admin.php`
- Look for missing semicolons, quotes, or brackets
### "Can't access wp-admin"
```bash
# Reset admin password
docker exec maple-wordpress-dev wp user update admin --user_pass=newpassword
```
### Build Includes Local Dev Files
**Verify build excludes:**
```bash
cd dist
unzip -l maplepress-plugin.zip | grep "wp-config.local"
# Should return nothing
unzip -l maplepress-plugin.zip | grep "Taskfile"
# Should return nothing
```
**If they're included, rebuild:**
```bash
task clean
task build
```
---
## Quick Reference
### Commands Cheat Sheet
```bash
# Infrastructure
cd cloud/infrastructure/development
task dev:start # Start all services
task dev:stop # Stop all services
task dev:status # Show service status
task dev:logs # View logs
# Plugin Development
cd native/wordpress/maplepress-plugin
task dev:setup # Set up local dev (first time)
task dev:reset # Reset to production config
task sync # Sync changes to WordPress
task watch # Auto-sync on file changes
task logs # View WordPress logs
task shell # Access WordPress container
# Production Release
task build # Build distribution zip
task clean # Clean build artifacts
# Testing
task lint # Check code style
task test # Run tests
```
### URLs
- WordPress Frontend: http://localhost:8081
- WordPress Admin: http://localhost:8081/wp-admin
- Plugin Settings: http://localhost:8081/wp-admin/options-general.php?page=maplepress
- Backend API: http://localhost:8000
### WordPress CLI (inside container)
```bash
# Access container
docker exec -it maple-wordpress-dev /bin/bash
# WordPress CLI commands
wp plugin list # List plugins
wp plugin activate maplepress
wp plugin deactivate maplepress
wp option get maplepress_settings
wp option delete maplepress_settings
wp user list # List users
wp db query "SELECT * FROM wp_options WHERE option_name LIKE 'maplepress%'"
```
---
## Next Steps
1. **For Development:**
- Follow "Local Development Setup"
- Run `task dev:setup`
- Start coding!
2. **For First Release:**
- Follow "Production Release Workflow"
- Build with `task build`
- Submit to WordPress.org
3. **Learn More:**
- WordPress Plugin Handbook: https://developer.wordpress.org/plugins/
- WordPress Coding Standards: https://developer.wordpress.org/coding-standards/
- Plugin Directory Guidelines: https://developer.wordpress.org/plugins/wordpress-org/
---
**Still stuck? Check:**
- `TESTING.md` - Testing scenarios and checklist
- `CHANGELOG.md` - Recent changes and updates
- Backend docs: `cloud/maplepress-backend/GETTING-STARTED.md` and `API.md`

View file

@ -0,0 +1,226 @@
# MaplePress Plugin Testing Guide
## Prerequisites
- WordPress running at http://localhost:8081
- MaplePress backend running at http://localhost:8000 (or update API URL in settings)
- WordPress admin access
## Test Scenarios
### Scenario 1: Fresh Installation (Activation Flow)
**Steps:**
1. **Deactivate the plugin** (if already activated)
- Go to WordPress Admin → Plugins
- Find "MaplePress"
- Click "Deactivate"
2. **Activate the plugin**
- Click "Activate" on MaplePress
- **Expected:** Auto-redirect to Settings → MaplePress
3. **Verify Welcome Screen**
- **Expected:** See "🚀 Welcome to MaplePress!" banner
- **Expected:** See 4-step setup instructions
- **Expected:** See "Sign Up at MaplePress.io" button
- **Expected:** See "Login to Existing Account" button
4. **Verify Admin Notice** (if you navigate away)
- Go to Dashboard or any other admin page
- **Expected:** See yellow warning banner at top
- **Expected:** Message says "MaplePress Setup Required"
- **Expected:** Contains links to sign up and configure
### Scenario 2: API Key Entry (Success Path)
**Prerequisites:**
- You have a valid API key from the backend
- To get one:
```bash
# Use the backend API to register and create a site
# Or use existing test API key
```
**Steps:**
1. **Go to Settings → MaplePress**
2. **Verify default API URL**
- **Expected:** API URL field shows `http://localhost:8000`
3. **Enter a valid API key**
- Paste your API key in the "API Key" field
- **Optional:** Check "Enable MaplePress"
- Click "Save Settings & Verify Connection"
4. **Verify Success Response**
- **Expected:** Green success message: "✓ API connection verified successfully!"
- **Expected:** Welcome banner disappears
- **Expected:** New section appears: "✓ Connected to MaplePress"
- **Expected:** Site details table shows:
- Site ID (UUID)
- Tenant ID (UUID)
- Domain (your site domain)
- Plan (e.g., "Free")
- Status (● Active or ● Inactive)
- **Expected:** "→ Open MaplePress Dashboard" button appears
5. **Verify Admin Notice Removed**
- Navigate to Dashboard or Posts
- **Expected:** Yellow warning banner is GONE
### Scenario 3: Invalid API Key (Error Path)
**Steps:**
1. **Go to Settings → MaplePress**
2. **Enter an invalid API key**
- Enter: `invalid_key_12345`
- Click "Save Settings & Verify Connection"
3. **Verify Error Response**
- **Expected:** Red error message appears
- **Expected:** Message says "API Connection Error: [error details]"
- **Expected:** Site details table does NOT appear
- **Expected:** Plugin status remains "needs setup"
4. **Verify Admin Notice Persists**
- Navigate to Dashboard
- **Expected:** Yellow warning banner STILL shows
### Scenario 4: Empty API Key
**Steps:**
1. **Go to Settings → MaplePress**
2. **Leave API key field empty**
- Click "Save Settings & Verify Connection"
3. **Verify Behavior**
- **Expected:** No error message (just saves empty)
- **Expected:** Welcome banner still shows
- **Expected:** Admin notice persists on other pages
### Scenario 5: API Key Update/Change
**Steps:**
1. **Start with a valid, connected API key**
- Verify you see "✓ Connected to MaplePress"
2. **Change to a different valid API key**
- Enter new API key
- Click "Save Settings & Verify Connection"
3. **Verify Update**
- **Expected:** Success message appears
- **Expected:** Site details update to reflect new site
4. **Change to invalid key**
- Enter invalid key
- Click save
5. **Verify Validation**
- **Expected:** Error message appears
- **Expected:** Connection status changes back to "needs setup"
- **Expected:** Admin notice returns
## Testing Checklist
- [ ] Plugin activation redirects to settings page
- [ ] Welcome banner shows on first visit
- [ ] "Sign Up" button links to https://getmaplepress.com/register (opens in new tab)
- [ ] "Login" button links to https://getmaplepress.com/login (opens in new tab)
- [ ] Admin notice appears on all pages when not configured
- [ ] Admin notice does NOT appear on settings page itself
- [ ] Admin notice is dismissible
- [ ] Valid API key shows success message
- [ ] Valid API key populates site details correctly
- [ ] Invalid API key shows clear error message
- [ ] Site details table shows after successful connection
- [ ] Site details include: site_id, tenant_id, domain, plan_tier
- [ ] Status indicator shows correct state (Active/Inactive)
- [ ] "Open MaplePress Dashboard" button appears when connected
- [ ] Dashboard button links to https://getmaplepress.com/dashboard (opens in new tab)
- [ ] Admin notice disappears after successful configuration
- [ ] Enable checkbox works correctly
- [ ] Settings persist after save
- [ ] API URL can be changed (for dev vs production)
## Manual API Key Testing
If you need to test with a real API key, follow the backend setup guide:
**See: `cloud/maplepress-backend/GETTING-STARTED.md` → "Create Test Data" section**
Quick summary:
1. Register a user with `POST /api/v1/register`
2. Create a site with `POST /api/v1/sites`
3. Save the `api_key` from the site creation response
For detailed curl examples and full instructions, see the backend GETTING-STARTED.md file.
## Expected Settings Structure
After successful configuration, WordPress options should contain:
```php
array(
'api_url' => 'http://localhost:8000',
'api_key' => 'live_sk_...',
'site_id' => 'abc-123-def',
'tenant_id' => 'xyz-456-uvw',
'domain' => 'localhost',
'plan_tier' => 'free',
'is_verified' => true,
'enabled' => true,
'needs_setup' => false,
)
```
## Troubleshooting
### Plugin doesn't redirect after activation
- Make sure you're not activating multiple plugins at once
- Try deactivating and reactivating
- Check if transient was set: `get_transient('maplepress_activation_redirect')`
### API connection fails
- Verify backend is running: `curl http://localhost:8000/health`
- Check API URL in settings matches backend URL
- Verify API key is correct and not expired
- Check WordPress debug logs: `docker exec -it maple-wordpress-dev tail -f /var/www/html/wp-content/debug.log`
- See backend troubleshooting: `cloud/maplepress-backend/GETTING-STARTED.md`
### Admin notice doesn't disappear
- Verify `needs_setup` is set to `false` in settings
- Clear WordPress transients/cache
- Deactivate and reactivate plugin
### Site details don't show
- Verify API response includes all fields (site_id, tenant_id, domain, plan_tier)
- Check backend `/api/v1/plugin/status` endpoint response
- Enable WordPress debug mode and check for PHP errors
## Success Criteria
✅ User can activate plugin and see setup instructions
✅ User can click external links to sign up/login
✅ User can enter API key and validate it
✅ Valid API key shows success and site details
✅ Invalid API key shows clear error
✅ Admin notices guide user through setup
✅ Connected state shows all site information
✅ Plugin can be enabled/disabled after connection
## Next Steps After Testing
Once basic authentication flow is verified:
1. Test with production backend URL
2. Implement content indexing features
3. Add search widget functionality
4. Build web dashboard for account/site management

View file

@ -0,0 +1,145 @@
version: '3'
tasks:
dev:setup:
desc: Set up local development environment
cmds:
- task create-local-config
- task inject-wp-config
- echo "Local development environment configured"
- echo "API URL pre-configured to http://maplepress-backend-dev:8000"
- echo "Go to Settings > MaplePress to enter your API key"
create-local-config:
desc: Create local development config file
cmds:
- |
cat > wp-config.local.php <<'EOF'
<?php
/**
* MaplePress Local Development Configuration
*
* This file is for LOCAL DEVELOPMENT ONLY.
* DO NOT commit this file to git.
* DO NOT include in production builds.
*/
// MaplePress Local Development Settings
// Uses Docker container hostname for inter-container communication
define('MAPLEPRESS_API_URL', 'http://maplepress-backend-dev:8000');
EOF
- echo "Created wp-config.local.php"
inject-wp-config:
desc: Add local config loader to WordPress wp-config.php
cmds:
- |
docker exec maple-wordpress-dev bash -c '
CONFIG_FILE="/var/www/html/wp-config.php"
LOADER_LINE="// MaplePress local development config\nif (file_exists(__DIR__ . '\''/wp-content/plugins/maplepress-plugin/wp-config.local.php'\'')) { require_once __DIR__ . '\''/wp-content/plugins/maplepress-plugin/wp-config.local.php'\''; }"
# Check if already added
if grep -q "maplepress-plugin/wp-config.local.php" "$CONFIG_FILE"; then
echo "wp-config.php already configured"
else
# Insert before "That'"'"'s all, stop editing!"
sed -i "/That.*s all, stop editing/i $LOADER_LINE" "$CONFIG_FILE"
echo "Added loader to wp-config.php"
fi
'
dev:reset:
desc: Reset to production configuration (remove local dev settings)
cmds:
- rm -f wp-config.local.php
- |
docker exec maple-wordpress-dev bash -c '
CONFIG_FILE="/var/www/html/wp-config.php"
# Remove MaplePress local config lines
sed -i "/MaplePress local development config/d" "$CONFIG_FILE"
sed -i "/maplepress-plugin\/wp-config.local.php/d" "$CONFIG_FILE"
'
- echo "Reset to production configuration"
- echo "Plugin will now use empty API URL (production behavior)"
sync:
desc: Sync plugin to local WordPress container
cmds:
- docker cp . maple-wordpress-dev:/var/www/html/wp-content/plugins/maplepress-plugin/
- echo "Plugin synced to WordPress container"
watch:
desc: Watch for changes and auto-sync to WordPress
cmds:
- |
echo "Watching for changes... (Press Ctrl+C to stop)"
fswatch -o . | while read; do
task sync
done
logs:
desc: View WordPress debug logs
cmds:
- docker exec -it maple-wordpress-dev tail -f /var/www/html/wp-content/debug.log
shell:
desc: Open shell in WordPress container
cmds:
- docker exec -it maple-wordpress-dev /bin/bash
lint:
desc: Run PHP CodeSniffer (requires phpcs)
cmds:
- phpcs --standard=WordPress --extensions=php .
lint:fix:
desc: Auto-fix PHP CodeSniffer issues (requires phpcbf)
cmds:
- phpcbf --standard=WordPress --extensions=php .
test:
desc: Run PHPUnit tests (requires phpunit)
cmds:
- phpunit
build:
desc: Build plugin zip for distribution
cmds:
- rm -rf dist
- mkdir -p dist
- |
rsync -av \
--exclude='dist' \
--exclude='.git' \
--exclude='node_modules' \
--exclude='Taskfile.yml' \
--exclude='.phpcs.xml.dist' \
--exclude='wp-config.local.php' \
--exclude='composer.json' \
--exclude='composer.lock' \
--exclude='vendor' \
--exclude='.gitignore' \
. dist/maplepress-plugin/
- cd dist && zip -r maplepress-plugin.zip maplepress-plugin
- echo "Plugin built at dist/maplepress-plugin.zip"
- echo "Local dev files excluded from build"
clean:
desc: Clean build artifacts
cmds:
- rm -rf dist
- rm -rf vendor
- rm -rf node_modules
- echo "Cleaned build artifacts"
install:
desc: Install Composer dependencies
cmds:
- composer install
- echo "Composer dependencies installed"
update:
desc: Update Composer dependencies
cmds:
- composer update
- echo "Composer dependencies updated"

View file

@ -0,0 +1,19 @@
/**
* MaplePress Admin Styles
*
* @package MaplePress
*/
.maplepress-admin {
max-width: 800px;
}
.maplepress-status-active {
color: #46b450;
font-weight: bold;
}
.maplepress-status-inactive {
color: #dc3232;
font-weight: bold;
}

View file

@ -0,0 +1,24 @@
/**
* MaplePress Public Styles
*
* @package MaplePress
*/
.maplepress-search-widget {
margin-bottom: 20px;
}
.maplepress-search-results {
margin-top: 20px;
}
.maplepress-search-result-item {
margin-bottom: 15px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.maplepress-search-result-item h3 {
margin-top: 0;
}

View file

@ -0,0 +1,440 @@
/**
* MaplePress Search Speed Test - Admin Styles
*/
.mpss-wrap {
max-width: 1200px;
margin: 20px auto 20px 0;
}
.mpss-card {
background: #fff;
border: 1px solid #ccd0d4;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
padding: 20px;
margin-bottom: 20px;
}
.mpss-card h2 {
margin-top: 0;
display: flex;
align-items: center;
gap: 10px;
}
.mpss-card h3 {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-top: 25px;
margin-bottom: 15px;
}
/* Mode Selection */
.mpss-mode-selection {
margin: 20px 0;
}
.mpss-radio-label {
display: block;
padding: 15px;
margin: 10px 0;
border: 2px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.mpss-radio-label:hover {
border-color: #2271b1;
background: #f9f9f9;
}
.mpss-radio-label input[type="radio"] {
margin-right: 10px;
}
.mpss-radio-label input[type="radio"]:checked + .mpss-radio-title {
color: #2271b1;
font-weight: 600;
}
.mpss-radio-label input[type="radio"]:checked ~ .mpss-radio-title {
color: #2271b1;
}
.mpss-radio-title {
font-weight: 600;
font-size: 14px;
display: block;
margin-bottom: 5px;
}
.mpss-radio-desc {
font-size: 13px;
color: #666;
display: block;
margin-left: 25px;
}
/* Test Info */
.mpss-test-info ul {
list-style: none;
margin: 10px 0;
padding: 0;
}
.mpss-test-info li {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.mpss-test-info li:last-child {
border-bottom: none;
}
/* Warning */
.mpss-warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px 15px;
margin: 20px 0;
display: flex;
align-items: center;
gap: 10px;
}
.mpss-warning .dashicons {
color: #856404;
flex-shrink: 0;
}
/* Progress */
.mpss-progress {
margin: 20px 0;
}
.mpss-progress-bar {
width: 100%;
height: 30px;
background: #f0f0f0;
border-radius: 15px;
overflow: hidden;
margin-top: 15px;
}
.mpss-progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
position: relative;
overflow: hidden;
}
.mpss-progress-fill.pulsing::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.mpss-progress-fill.pulsing {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Test Meta */
.mpss-test-meta {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin: 15px 0 25px 0;
padding: 15px;
background: #f9f9f9;
border-radius: 4px;
}
.mpss-test-meta span {
font-size: 14px;
}
.mpss-test-meta strong {
color: #2271b1;
}
/* Summary Cards */
.mpss-summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin: 20px 0;
}
.mpss-profile-card {
border: 2px solid #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: transform 0.2s;
}
.mpss-profile-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.mpss-profile-card h4 {
margin: 0 0 15px 0;
font-size: 16px;
color: #333;
}
.mpss-profile-metrics {
display: flex;
justify-content: space-around;
margin: 15px 0;
}
.mpss-metric {
text-align: center;
}
.mpss-metric-label {
font-size: 11px;
color: #666;
text-transform: uppercase;
display: block;
margin-bottom: 5px;
}
.mpss-metric-value {
font-size: 18px;
font-weight: 600;
display: block;
}
.mpss-status-badge {
display: inline-block;
padding: 5px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-top: 10px;
}
.mpss-status-excellent {
background: #d4edda;
color: #155724;
border-color: #46b450;
}
.mpss-status-good {
background: #d1ecf1;
color: #0c5460;
border-color: #00a0d2;
}
.mpss-status-fair {
background: #fff3cd;
color: #856404;
border-color: #ffc107;
}
.mpss-status-poor {
background: #f8d7da;
color: #721c24;
border-color: #dc3545;
}
.mpss-status-critical {
background: #dc3232;
color: #fff;
border-color: #dc3232;
}
.mpss-profile-details {
font-size: 12px;
color: #666;
margin-top: 10px;
}
/* Chart Container */
.mpss-chart-container {
margin: 30px 0;
padding: 20px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
}
#mpss-chart {
max-height: 400px;
}
/* Results Table */
.mpss-table-container {
margin: 30px 0;
}
#mpss-results-table {
margin-top: 15px;
}
#mpss-results-table th {
font-weight: 600;
}
#mpss-results-table td {
vertical-align: middle;
}
/* Recommendations */
.mpss-recommendations {
margin: 30px 0;
}
.mpss-recommendation {
padding: 15px;
margin: 15px 0;
border-left: 4px solid;
border-radius: 4px;
}
.mpss-recommendation h4 {
margin: 0 0 10px 0;
font-size: 14px;
}
.mpss-recommendation p {
margin: 0;
font-size: 13px;
}
.mpss-recommendation-success {
background: #d4edda;
border-color: #46b450;
color: #155724;
}
.mpss-recommendation-info {
background: #d1ecf1;
border-color: #00a0d2;
color: #0c5460;
}
.mpss-recommendation-warning {
background: #fff3cd;
border-color: #ffc107;
color: #856404;
}
.mpss-recommendation-error {
background: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.mpss-recommendation-critical {
background: #dc3232;
border-color: #dc3232;
color: #fff;
}
/* Site Info */
.mpss-site-info {
margin: 30px 0;
padding: 20px;
background: #f9f9f9;
border-radius: 4px;
}
.mpss-site-info ul {
list-style: none;
margin: 10px 0;
padding: 0;
}
.mpss-site-info li {
padding: 8px 0;
font-size: 14px;
}
.mpss-site-info strong {
color: #2271b1;
min-width: 150px;
display: inline-block;
}
/* Actions */
.mpss-actions {
display: flex;
gap: 10px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
}
.mpss-actions button {
display: flex;
align-items: center;
gap: 5px;
}
/* Responsive */
@media (max-width: 768px) {
.mpss-summary-cards {
grid-template-columns: 1fr;
}
.mpss-test-meta {
flex-direction: column;
gap: 10px;
}
.mpss-actions {
flex-direction: column;
}
.mpss-actions button {
width: 100%;
justify-content: center;
}
}

View file

@ -0,0 +1,15 @@
/**
* MaplePress Admin JavaScript
*
* @package MaplePress
*/
(function($) {
'use strict';
$(document).ready(function() {
// Admin-specific JavaScript can be added here
console.log('MaplePress Admin JS loaded');
});
})(jQuery);

View file

@ -0,0 +1,15 @@
/**
* MaplePress Public JavaScript
*
* @package MaplePress
*/
(function($) {
'use strict';
$(document).ready(function() {
// Public-facing JavaScript can be added here
console.log('MaplePress Public JS loaded');
});
})(jQuery);

View file

@ -0,0 +1,894 @@
/**
* MaplePress Search Speed Test - Simple Admin JavaScript
*/
(function($) {
'use strict';
var testResults = null;
var performanceChart = null;
$(document).ready(function() {
// Note: Notification permission will be requested when user runs first test
// (requestNotificationPermission() can only be called from user gesture)
// Run test button
$('#mpss-run-test').on('click', function() {
runSpeedTest();
});
// Cancel test button
$(document).on('click', '#mpss-cancel-test', function() {
if (confirm('Are you sure you want to cancel the test?')) {
cancelTest();
}
});
// Run again button
$(document).on('click', '#mpss-run-again', function() {
resetTest();
});
// Export results button
$(document).on('click', '#mpss-export-results', function() {
exportResults();
});
// Share results button
$(document).on('click', '#mpss-share-results', function() {
showShareModal();
});
// Close share modal
$(document).on('click', '#mpss-close-share-modal', function() {
closeShareModal();
});
// Share buttons
$(document).on('click', '.mpss-share-twitter', function() {
shareToTwitter();
});
$(document).on('click', '.mpss-share-facebook', function() {
shareToFacebook();
});
$(document).on('click', '.mpss-share-linkedin', function() {
shareToLinkedIn();
});
$(document).on('click', '.mpss-share-copy', function() {
copyShareLink();
});
// Handle mode card selection
$('.mpss-mode-card').on('click', function() {
var $radio = $(this).find('input[type="radio"]');
$radio.prop('checked', true);
// Update visual state of cards
$('.mpss-mode-card-inner').css({
'border-color': '#e0e0e0',
'box-shadow': '0 1px 3px rgba(0, 0, 0, 0.05)',
'transform': 'scale(1)'
});
$(this).find('.mpss-mode-card-inner').css({
'border-color': '#667eea',
'box-shadow': '0 4px 15px rgba(102, 126, 234, 0.3)',
'transform': 'scale(1.02)'
});
});
// Add hover effects for mode cards
$('.mpss-mode-card').hover(
function() {
if (!$(this).find('input[type="radio"]').is(':checked')) {
$(this).find('.mpss-mode-card-inner').css({
'transform': 'translateY(-2px)',
'box-shadow': '0 4px 10px rgba(0, 0, 0, 0.1)'
});
}
},
function() {
if (!$(this).find('input[type="radio"]').is(':checked')) {
$(this).find('.mpss-mode-card-inner').css({
'transform': 'scale(1)',
'box-shadow': '0 1px 3px rgba(0, 0, 0, 0.05)'
});
}
}
);
// Add hover effect for run button
$('#mpss-run-test').hover(
function() {
$(this).css({
'transform': 'translateY(-2px)',
'box-shadow': '0 6px 20px rgba(102, 126, 234, 0.5)'
});
},
function() {
$(this).css({
'transform': 'translateY(0)',
'box-shadow': '0 4px 15px rgba(102, 126, 234, 0.4)'
});
}
);
// Add focus effect for select dropdown
$('#mpss-query-count').on('focus', function() {
$(this).css({
'border-color': '#667eea',
'box-shadow': '0 0 0 3px rgba(102, 126, 234, 0.1)'
});
}).on('blur', function() {
$(this).css({
'border-color': '#e0e0e0',
'box-shadow': 'none'
});
});
// Initialize the first card as selected
$('.mpss-mode-card').first().find('.mpss-mode-card-inner').css({
'border-color': '#667eea',
'box-shadow': '0 4px 15px rgba(102, 126, 234, 0.3)',
'transform': 'scale(1.02)'
});
});
var progressInterval = null;
var currentAjaxRequest = null;
function runSpeedTest() {
// Request notification permission on first test run
requestNotificationPermission();
var queryCount = parseInt($('#mpss-query-count').val()) || 10;
var executionMode = $('input[name="execution_mode"]:checked').val() || 'serial';
console.log('Starting speed test with ' + queryCount + ' queries in ' + executionMode + ' mode');
console.log('Execution mode value:', executionMode);
console.log('Radio button checked:', $('input[name="execution_mode"]:checked').length);
// Hide config, show progress
$('.mpss-test-config').fadeOut();
$('#mpss-results').hide();
$('#mpss-progress').fadeIn();
var modeText = executionMode === 'parallel' ? 'many at once' : 'one at a time';
$('#mpss-progress-text').text('Testing ' + queryCount + ' searches (' + modeText + ')...');
$('#mpss-progress-fill').css('width', '10%');
// Initialize counter
$('#mpss-total-count').text(queryCount);
$('#mpss-completed-count').text(0);
// Calculate and display estimated time
var estimatedSeconds = Math.ceil((queryCount * 200) / 1000); // 200ms per query
if (executionMode === 'parallel') {
estimatedSeconds = Math.ceil(estimatedSeconds * 0.1); // Parallel is much faster
}
updateTimeEstimate(estimatedSeconds);
// Start animated progress counter
startProgressAnimation(queryCount, estimatedSeconds);
// Make AJAX request
currentAjaxRequest = $.ajax({
url: mpssAjax.ajaxurl,
method: 'POST',
data: {
action: 'mpss_run_test',
nonce: mpssAjax.nonce,
query_count: queryCount,
execution_mode: executionMode
},
success: function(response) {
currentAjaxRequest = null;
console.log('Response received:', response);
// Stop the progress animation
stopProgressAnimation();
if (response.success) {
testResults = response.data;
console.log('Test results:', testResults);
// Complete the progress
$('#mpss-completed-count').text(queryCount);
$('#mpss-progress-fill').css('width', '100%');
$('#mpss-progress-text').text('Test complete!');
setTimeout(function() {
displayResults(testResults);
}, 500);
} else {
showError('Test failed: ' + (response.data ? response.data.message : 'Unknown error'));
}
},
error: function(xhr, status, error) {
currentAjaxRequest = null;
// Don't show error if request was aborted (cancelled by user)
if (status === 'abort') {
return;
}
console.error('AJAX error:', error);
stopProgressAnimation();
showError('Network error: ' + error);
}
});
}
function cancelTest() {
// Abort the AJAX request
if (currentAjaxRequest) {
currentAjaxRequest.abort();
currentAjaxRequest = null;
}
// Stop the animation
stopProgressAnimation();
// Hide progress and show config
$('#mpss-progress').fadeOut(function() {
$('.mpss-test-config').fadeIn();
});
// Show cancellation message
alert('Test cancelled');
}
function updateTimeEstimate(secondsRemaining) {
if (secondsRemaining <= 0) {
$('#mpss-time-estimate').text('Almost done...');
return;
}
var minutes = Math.floor(secondsRemaining / 60);
var seconds = secondsRemaining % 60;
var timeText = 'Estimated time: ';
if (minutes > 0) {
timeText += minutes + ' min ' + seconds + ' sec';
} else {
timeText += seconds + ' sec';
}
$('#mpss-time-estimate').text(timeText);
}
function startProgressAnimation(totalCount, estimatedSeconds) {
// Clear any existing interval
if (progressInterval) {
clearInterval(progressInterval);
}
var currentCount = 0;
var estimatedTime = totalCount * 200; // Rough estimate: 200ms per search
var updateInterval = 100; // Update every 100ms
var incrementPerUpdate = totalCount / (estimatedTime / updateInterval);
var reachedNinetyFive = false;
var timeRemaining = estimatedSeconds;
var lastTimeUpdate = Date.now();
progressInterval = setInterval(function() {
// Update time estimate every second
var now = Date.now();
if (now - lastTimeUpdate >= 1000) {
timeRemaining--;
updateTimeEstimate(timeRemaining);
lastTimeUpdate = now;
}
// After reaching 95%, slow down significantly
if (currentCount >= totalCount * 0.95 && !reachedNinetyFive) {
reachedNinetyFive = true;
incrementPerUpdate = incrementPerUpdate * 0.1; // Slow down to 10% speed
// Update the message and add pulsing animation
$('#mpss-progress-text').html('Almost done, finalizing results...');
$('#mpss-progress-fill').addClass('pulsing');
}
currentCount += incrementPerUpdate;
// Cap at 99% until we get the real result
var displayCount = Math.min(Math.floor(currentCount), Math.floor(totalCount * 0.99));
var percentage = Math.min((displayCount / totalCount) * 100, 99);
$('#mpss-completed-count').text(displayCount);
$('#mpss-progress-fill').css('width', percentage + '%');
// Stop at 99%
if (displayCount >= totalCount * 0.99) {
clearInterval(progressInterval);
progressInterval = null;
}
}, updateInterval);
}
function stopProgressAnimation() {
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
}
function displayResults(data) {
console.log('Displaying results...');
// Calculate average time for notification
var avgTime = data.total_time / data.total_queries;
// Play completion sound and show notification
playCompletionSound();
showTestCompleteNotification(avgTime, data.total_queries);
// Hide progress, show results
$('#mpss-progress').fadeOut();
$('#mpss-results').fadeIn();
// Display recommendations
displayRecommendations(data.total_queries, data.mode);
// Display summary stats (only total queries and total time)
$('#mpss-total-queries').text(data.total_queries.toLocaleString());
$('#mpss-total-time').text(data.total_time + ' sec');
// Add mode-specific explanation
var $chart = $('#mpss-chart');
var existingExplanation = $chart.parent().find('.mpss-mode-explanation');
if (existingExplanation.length) {
existingExplanation.remove();
}
var explanation = '';
if (data.mode === 'parallel') {
explanation = '<div class="mpss-mode-explanation" style="padding: 15px; margin-bottom: 20px; background: linear-gradient(to right, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1)); border-left: 4px solid #667eea; border-radius: 8px;">' +
'<strong style="color: #667eea; font-size: 14px;">👥 Stress Test Mode:</strong><br>' +
'<span style="font-size: 13px; color: #666; line-height: 1.6;">' +
'We simulated <strong>' + data.total_queries + ' people</strong> searching your site at the same time. It took <strong>' + data.total_time + ' seconds</strong> to handle everyone.' +
'</span>' +
'</div>';
} else {
explanation = '<div class="mpss-mode-explanation" style="padding: 15px; margin-bottom: 20px; background: linear-gradient(to right, rgba(240, 147, 251, 0.1), rgba(245, 87, 108, 0.1)); border-left: 4px solid #f093fb; border-radius: 8px;">' +
'<strong style="color: #f093fb; font-size: 14px;">👤 Normal Mode:</strong><br>' +
'<span style="font-size: 13px; color: #666; line-height: 1.6;">' +
'We tested <strong>' + data.total_queries + ' searches</strong> one at a time. The total test took <strong>' + data.total_time + ' seconds</strong> to complete.' +
'</span>' +
'</div>';
}
$chart.parent().prepend(explanation);
// Display chart
displayChart(data);
}
function displayChart(data) {
var chartContainer = $('#mpss-chart').parent();
// Destroy existing chart
if (performanceChart) {
performanceChart.destroy();
performanceChart = null;
}
// Restore the canvas if needed
if (!chartContainer.find('canvas').length) {
chartContainer.html('<canvas id="mpss-chart"></canvas>');
}
var ctx = document.getElementById('mpss-chart').getContext('2d');
// Prepare data
var labels = data.results.map(function(r, i) { return 'Query ' + (i + 1); });
var times = data.results.map(function(r) { return r.duration_ms; });
// Determine title based on mode
var chartTitle = 'How Long Each Search Took';
var chartSubtitle = '';
if (data.mode === 'parallel') {
chartSubtitle = 'Each dot shows how long one search took (all happening at the same time)';
} else {
chartSubtitle = 'Each dot shows how long one search took (one after another)';
}
// Create chart
performanceChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Response Time (ms)',
data: times,
backgroundColor: 'rgba(102, 126, 234, 0.3)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 2,
pointRadius: 3,
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: chartTitle
},
subtitle: {
display: true,
text: chartSubtitle,
font: {
size: 11,
style: 'italic'
},
color: '#666'
}
},
scales: {
x: {
display: false
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Response Time (ms)'
}
}
}
}
});
}
function exportResults() {
if (!testResults) {
alert('No results to export');
return;
}
// Calculate metrics
var avgTime = testResults.total_time / testResults.total_queries;
var avgMs = (avgTime * 1000).toFixed(0);
var modeLabel = testResults.mode === 'parallel' ? 'Stress Test - Many People at Once' : 'Normal - One Person at a Time';
var date = new Date();
var dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
// Calculate statistics
var times = testResults.results.map(function(r) { return r.duration_ms; });
var minTime = Math.min.apply(null, times);
var maxTime = Math.max.apply(null, times);
var medianTime = calculateMedian(times);
// Build HTML report
var html = '<!DOCTYPE html>\n';
html += '<html lang="en">\n';
html += '<head>\n';
html += ' <meta charset="UTF-8">\n';
html += ' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n';
html += ' <title>Search Speed Test Report - ' + dateStr + '</title>\n';
html += ' <style>\n';
html += ' * { margin: 0; padding: 0; box-sizing: border-box; }\n';
html += ' body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f5f5f5; padding: 40px 20px; color: #333; line-height: 1.6; }\n';
html += ' .container { max-width: 1000px; margin: 0 auto; background: white; padding: 50px; border-radius: 20px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); }\n';
html += ' .header { text-align: center; margin-bottom: 50px; padding-bottom: 30px; border-bottom: 3px solid #667eea; }\n';
html += ' .header h1 { font-size: 42px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 15px; }\n';
html += ' .header .date { font-size: 16px; color: #666; }\n';
html += ' .summary-section { text-align: center; padding: 40px; background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1)); border-radius: 16px; margin-bottom: 40px; border: 3px solid #667eea; }\n';
html += ' .summary-icon { font-size: 80px; margin-bottom: 20px; }\n';
html += ' .summary-title { font-size: 32px; font-weight: 700; color: #667eea; margin-bottom: 15px; }\n';
html += ' .summary-message { font-size: 18px; color: #666; }\n';
html += ' .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; }\n';
html += ' .metric { background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1)); padding: 25px; border-radius: 12px; text-align: center; }\n';
html += ' .metric-label { font-size: 13px; color: #666; text-transform: uppercase; font-weight: 600; margin-bottom: 10px; }\n';
html += ' .metric-value { font-size: 32px; font-weight: 800; color: #667eea; }\n';
html += ' .section { margin-bottom: 40px; }\n';
html += ' .section h2 { font-size: 24px; color: #333; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 2px solid #e0e0e0; }\n';
html += ' .info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; }\n';
html += ' .info-item { padding: 15px; background: #f9f9f9; border-radius: 8px; }\n';
html += ' .info-label { font-size: 12px; color: #666; text-transform: uppercase; margin-bottom: 5px; }\n';
html += ' .info-value { font-size: 18px; font-weight: 600; color: #333; }\n';
html += ' .footer { text-align: center; padding-top: 30px; margin-top: 50px; border-top: 2px solid #e0e0e0; color: #999; font-size: 14px; }\n';
html += ' @media print { body { background: white; padding: 0; } .container { box-shadow: none; padding: 30px; } }\n';
html += ' </style>\n';
html += '</head>\n';
html += '<body>\n';
html += ' <div class="container">\n';
html += ' <div class="header">\n';
html += ' <h1>Search Speed Test Report</h1>\n';
html += ' <div class="date">Generated on ' + escapeHtml(dateStr) + '</div>\n';
html += ' </div>\n';
html += ' \n';
html += ' <div class="summary-section">\n';
html += ' <div class="summary-icon">🚀</div>\n';
html += ' <div class="summary-title">Search Speed Test Results</div>\n';
html += ' <div class="summary-message">Average response time: ' + avgMs + ' ms</div>\n';
html += ' </div>\n';
html += ' \n';
html += ' <div class="metrics">\n';
html += ' <div class="metric">\n';
html += ' <div class="metric-label">Average Time</div>\n';
html += ' <div class="metric-value">' + avgMs + ' ms</div>\n';
html += ' </div>\n';
html += ' <div class="metric">\n';
html += ' <div class="metric-label">Total Tests</div>\n';
html += ' <div class="metric-value">' + testResults.total_queries + '</div>\n';
html += ' </div>\n';
html += ' <div class="metric">\n';
html += ' <div class="metric-label">Total Time</div>\n';
html += ' <div class="metric-value">' + testResults.total_time + ' s</div>\n';
html += ' </div>\n';
html += ' <div class="metric">\n';
html += ' <div class="metric-label">Test Mode</div>\n';
html += ' <div class="metric-value" style="font-size: 18px;">' + escapeHtml(modeLabel.split(' - ')[0]) + '</div>\n';
html += ' </div>\n';
html += ' </div>\n';
html += ' \n';
html += ' <div class="section">\n';
html += ' <h2>Detailed Statistics</h2>\n';
html += ' <div class="info-grid">\n';
html += ' <div class="info-item">\n';
html += ' <div class="info-label">Fastest Response</div>\n';
html += ' <div class="info-value">' + minTime.toFixed(0) + ' ms</div>\n';
html += ' </div>\n';
html += ' <div class="info-item">\n';
html += ' <div class="info-label">Slowest Response</div>\n';
html += ' <div class="info-value">' + maxTime.toFixed(0) + ' ms</div>\n';
html += ' </div>\n';
html += ' <div class="info-item">\n';
html += ' <div class="info-label">Median Response</div>\n';
html += ' <div class="info-value">' + medianTime.toFixed(0) + ' ms</div>\n';
html += ' </div>\n';
html += ' <div class="info-item">\n';
html += ' <div class="info-label">Test Mode</div>\n';
html += ' <div class="info-value">' + escapeHtml(modeLabel) + '</div>\n';
html += ' </div>\n';
html += ' </div>\n';
html += ' </div>\n';
html += ' \n';
html += ' <div class="section">\n';
html += ' <h2>General Recommendations</h2>\n';
html += ' <div style="background: rgba(102, 126, 234, 0.1); padding: 25px; border-radius: 12px; border-left: 4px solid #667eea;">\n';
html += ' <p><strong>💡 Optimization Tips</strong></p>\n';
html += ' <p>Consider these strategies to improve search performance:</p>\n';
html += ' <ul style="margin-top: 15px; margin-left: 20px;">\n';
html += ' <li>Install and configure a caching plugin (e.g., WP Super Cache, W3 Total Cache)</li>\n';
html += ' <li>Optimize your database with plugins like WP-Optimize</li>\n';
html += ' <li>Review and optimize database indexes</li>\n';
html += ' <li>Consider upgrading your hosting plan for better resources</li>\n';
html += ' <li>Monitor performance regularly as your site grows</li>\n';
html += ' </ul>\n';
html += ' </div>\n';
html += ' </div>\n';
html += ' \n';
html += ' <div class="footer">\n';
html += ' <p>Generated by MaplePress Search Speed Test Plugin</p>\n';
html += ' <p>Website: ' + escapeHtml(window.location.hostname) + '</p>\n';
html += ' </div>\n';
html += ' </div>\n';
html += '</body>\n';
html += '</html>';
// Create and download the HTML file
var htmlBlob = new Blob([html], {type: 'text/html'});
var url = URL.createObjectURL(htmlBlob);
var link = document.createElement('a');
link.href = url;
link.download = 'search-speed-test-report-' + Date.now() + '.html';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
// Show success message
alert('✅ Report exported! Open the HTML file in your browser and use "Print to PDF" to create a PDF version.');
}
function calculateMedian(arr) {
var sorted = arr.slice().sort(function(a, b) { return a - b; });
var mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return (sorted[mid - 1] + sorted[mid]) / 2;
}
return sorted[mid];
}
function resetTest() {
$('#mpss-results').fadeOut();
$('.mpss-test-config').fadeIn();
testResults = null;
}
function showError(message) {
$('#mpss-progress').fadeOut();
$('.mpss-test-config').fadeIn();
alert('Error: ' + message);
}
function displayRecommendations(totalQueries, mode) {
var recommendations = [];
// Query count recommendations
if (totalQueries < 100) {
recommendations.push({
type: 'info',
icon: '📊',
title: 'Try more searches',
message: 'Testing with ' + totalQueries + ' searches gives you a basic idea. Try 500-1000 searches for more accurate results.'
});
}
// Mode-based recommendations
if (mode === 'serial') {
recommendations.push({
type: 'info',
icon: '🔄',
title: 'Try Stress Test mode',
message: 'You tested in Normal mode. Try "Stress Test - Many People at Once" to see how your site handles high traffic.'
});
} else {
recommendations.push({
type: 'info',
icon: '👥',
title: 'Consider traffic patterns',
message: 'Stress Test mode shows worst-case performance. If your site rarely gets simultaneous searches, Normal mode results matter more.'
});
}
// Build HTML
var html = '<div style="margin: 30px 0;">';
html += '<h3 style="margin-bottom: 20px; font-size: 18px; color: #333;">💡 Quick Tips</h3>';
for (var i = 0; i < recommendations.length; i++) {
var rec = recommendations[i];
var bgColor, borderColor;
if (rec.type === 'success') {
bgColor = '#d1fae5';
borderColor = '#10b981';
} else if (rec.type === 'info') {
bgColor = '#dbeafe';
borderColor = '#3b82f6';
} else if (rec.type === 'warning') {
bgColor = '#fef3c7';
borderColor = '#f59e0b';
} else {
bgColor = '#fee2e2';
borderColor = '#ef4444';
}
html += '<div style="padding: 15px; margin: 15px 0; background: ' + bgColor + '; border-left: 4px solid ' + borderColor + '; border-radius: 8px;">';
html += '<div style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 8px;">' + rec.icon + ' ' + rec.title + '</div>';
html += '<div style="font-size: 14px; color: #666; line-height: 1.6;">' + rec.message + '</div>';
html += '</div>';
}
html += '</div>';
$('#mpss-recommendations').html(html);
}
function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
}
function showShareModal() {
if (!testResults) {
alert('No results to share');
return;
}
// Calculate metrics
var avgTime = testResults.total_time / testResults.total_queries;
var avgMs = (avgTime * 1000).toFixed(0);
var modeLabel = testResults.mode === 'parallel' ? 'Stress Test' : 'Normal';
// Build share graphic
var graphicHtml = '<div style="text-align: center;">';
graphicHtml += '<div style="font-size: 64px; margin-bottom: 15px;">🚀</div>';
graphicHtml += '<div style="font-size: 28px; font-weight: 700; margin-bottom: 10px;">My Search Speed Test Results</div>';
graphicHtml += '<div style="font-size: 48px; font-weight: 800; margin: 20px 0; text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);">' + avgMs + ' ms</div>';
graphicHtml += '<div style="font-size: 18px; opacity: 0.9; margin-bottom: 10px;">Average Response Time</div>';
graphicHtml += '<div style="font-size: 14px; opacity: 0.8; margin-top: 20px;">Tested ' + testResults.total_queries + ' searches in ' + modeLabel + ' mode</div>';
graphicHtml += '</div>';
$('#mpss-share-graphic').html(graphicHtml);
// Show modal with flex display
$('#mpss-share-modal').css('display', 'flex').hide().fadeIn(300);
}
function closeShareModal() {
$('#mpss-share-modal').fadeOut(300);
}
function shareToTwitter() {
if (!testResults) return;
var avgTime = testResults.total_time / testResults.total_queries;
var avgMs = (avgTime * 1000).toFixed(0);
var text = 'I just tested my website\'s search speed! 🚀 Average response time: ' + avgMs + 'ms #WebPerformance #SiteSpeed';
var url = window.location.href;
var twitterUrl = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text) + '&url=' + encodeURIComponent(url);
window.open(twitterUrl, '_blank', 'width=550,height=420');
}
function shareToFacebook() {
if (!testResults) return;
var url = window.location.href;
var facebookUrl = 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url);
window.open(facebookUrl, '_blank', 'width=550,height=420');
}
function shareToLinkedIn() {
if (!testResults) return;
var avgTime = testResults.total_time / testResults.total_queries;
var avgMs = (avgTime * 1000).toFixed(0);
var url = window.location.href;
var title = 'Website Search Speed Test Results';
var summary = 'I tested my website\'s search performance. Average response time: ' + avgMs + 'ms';
var linkedinUrl = 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(url);
window.open(linkedinUrl, '_blank', 'width=550,height=420');
}
function copyShareLink() {
if (!testResults) return;
var avgTime = testResults.total_time / testResults.total_queries;
var avgMs = (avgTime * 1000).toFixed(0);
var text = 'I just tested my website\'s search speed! 🚀 Average response time: ' + avgMs + 'ms\n' + window.location.href;
// Try to use modern clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('✅ Link copied to clipboard!');
}).catch(function() {
fallbackCopyText(text);
});
} else {
fallbackCopyText(text);
}
}
function fallbackCopyText(text) {
// Fallback for older browsers
var textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('✅ Link copied to clipboard!');
} catch (err) {
alert('❌ Could not copy to clipboard. Please copy manually:\n\n' + text);
}
document.body.removeChild(textArea);
}
function requestNotificationPermission() {
// Check if browser supports notifications
if (!('Notification' in window)) {
console.log('This browser does not support desktop notifications');
return;
}
// Request permission if not already granted or denied
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
function showTestCompleteNotification(avgTime, totalQueries) {
// Check if notifications are supported and permitted
if (!('Notification' in window) || Notification.permission !== 'granted') {
return;
}
var avgMs = (avgTime * 1000).toFixed(0);
var title = 'Search Speed Test Complete!';
var body = 'Tested ' + totalQueries + ' searches. Average response time: ' + avgMs + 'ms';
var icon = ''; // WordPress doesn't have a default icon, but we could add one
try {
var notification = new Notification(title, {
body: body,
icon: icon,
badge: icon,
tag: 'mpss-test-complete',
requireInteraction: false
});
// Auto-close notification after 5 seconds
setTimeout(function() {
notification.close();
}, 5000);
// Focus window when notification is clicked
notification.onclick = function() {
window.focus();
notification.close();
};
} catch (e) {
console.log('Could not show notification:', e);
}
}
function playCompletionSound() {
// Create a simple success sound using Web Audio API
try {
var audioContext = new (window.AudioContext || window.webkitAudioContext)();
var oscillator = audioContext.createOscillator();
var gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// Set sound parameters for a pleasant "ding" sound
oscillator.frequency.value = 800; // Start frequency
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
// Add a second tone for a pleasant chord
setTimeout(function() {
var oscillator2 = audioContext.createOscillator();
var gainNode2 = audioContext.createGain();
oscillator2.connect(gainNode2);
gainNode2.connect(audioContext.destination);
oscillator2.frequency.value = 1000;
oscillator2.type = 'sine';
gainNode2.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4);
oscillator2.start(audioContext.currentTime);
oscillator2.stop(audioContext.currentTime + 0.4);
}, 100);
} catch (e) {
console.log('Could not play sound:', e);
}
}
})(jQuery);

View file

@ -0,0 +1,35 @@
{
"name": "mapleopentech/maplepress-plugin",
"description": "Cloud services platform for WordPress - offloads computationally intensive tasks to improve performance. Features search, with uploads, metrics, and more coming soon",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Maple Open Technologies",
"email": "hello@mapleopentech.io"
}
],
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"wp-coding-standards/wpcs": "^3.0",
"squizlabs/php_codesniffer": "^3.7"
},
"autoload": {
"psr-4": {
"MaplePress\\": "includes/"
}
},
"scripts": {
"lint": "phpcs --standard=WordPress --extensions=php .",
"lint:fix": "phpcbf --standard=WordPress --extensions=php .",
"test": "phpunit"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View file

@ -0,0 +1,342 @@
# MaplePress Performance Optimization Guide
## Understanding WordPress Performance Bottlenecks
MaplePress offloads search processing to our cloud infrastructure, but your WordPress site's ability to **handle concurrent requests** determines the actual user experience. Even with our lightning-fast backend (< 20ms processing time), users may experience slow response times if WordPress itself is the bottleneck.
---
## The #1 Bottleneck: PHP-FPM Worker Starvation
### What is PHP-FPM?
**PHP-FPM** (FastCGI Process Manager) is the process manager that handles PHP requests in WordPress. It uses a pool of "worker" processes to execute PHP code.
**Think of it like a restaurant:**
- **Chef (PHP-FPM worker)** = processes one request at a time
- **Order (HTTP request)** = incoming search/page load
If you have **5 chefs** and **25 orders arrive simultaneously**, only 5 orders get processed immediately. The other 20 orders **wait in line** (queue), even if each order only takes 200ms to prepare.
### Real-World Impact
**Common Default Configuration:**
```ini
pm.max_children = 5 # Only 5 workers!
```
**What This Means:**
- **Concurrent users**: 25 people search simultaneously
- **Workers available**: Only 5
- **Result**: 20 requests wait in queue
- **User experience**: 8-10 second response times (even though backend is fast!)
**Performance Comparison (25 concurrent searches):**
| Configuration | Workers | Response Time | Queue Time |
|--------------|---------|---------------|------------|
| **Default** | 5 | 8,000-9,000ms | ~7,800ms waiting |
| **Optimized** | 50 | 300-500ms | No waiting ✅ |
**52x faster** just by increasing workers!
---
## Solution: Increase PHP-FPM Workers
### Step 1: Check Current Configuration
SSH into your WordPress server:
```bash
# For standard PHP-FPM installations
sudo cat /etc/php/*/fpm/pool.d/www.conf | grep -E "^pm\.|^pm "
# For Docker (check inside container)
docker exec your-wordpress-container cat /usr/local/etc/php-fpm.d/www.conf | grep -E "^pm\.|^pm "
```
Look for these values:
```ini
pm = dynamic
pm.max_children = 5 # ← This is usually the problem!
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
```
### Step 2: Calculate Optimal Worker Count
**Formula:**
```
pm.max_children = (Available RAM) / (Average PHP Memory per Process)
```
**Example Calculations:**
**2GB Server:**
- Total RAM: 2048 MB
- System RAM: ~512 MB
- Available: 1536 MB
- PHP Memory per Process: ~70 MB
- **Recommended**: `pm.max_children = 20-25`
**4GB Server:**
- Total RAM: 4096 MB
- System RAM: ~512 MB
- Available: 3584 MB
- PHP Memory per Process: ~70 MB
- **Recommended**: `pm.max_children = 50`
**8GB Server:**
- Total RAM: 8192 MB
- System RAM: ~512 MB
- Available: 7680 MB
- PHP Memory per Process: ~70 MB
- **Recommended**: `pm.max_children = 100`
### Step 3: Apply Configuration
**For Docker Deployments** (Recommended):
Create `/opt/wordpress/php-fpm-custom.conf`:
```ini
[www]
pm = dynamic
pm.max_children = 50 ; Adjust based on your RAM
pm.start_servers = 10 ; Start with 10 workers
pm.min_spare_servers = 5 ; Keep at least 5 idle
pm.max_spare_servers = 20 ; Keep at most 20 idle
pm.max_requests = 500 ; Recycle workers after 500 requests
; Logging for debugging
pm.status_path = /status
ping.path = /ping
ping.response = pong
```
Mount it in `docker-compose.yml`:
```yaml
wordpress:
volumes:
- ./php-fpm-custom.conf:/usr/local/etc/php-fpm.d/zz-custom.conf:ro
```
Restart: `docker compose restart wordpress`
**For Standard PHP-FPM Installations**:
```bash
# Edit PHP-FPM config (adjust PHP version as needed)
sudo nano /etc/php/8.2/fpm/pool.d/www.conf
# Find and modify:
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
# Restart PHP-FPM
sudo systemctl restart php8.2-fpm
# Verify
sudo systemctl status php8.2-fpm
```
### Step 4: Verify Changes
```bash
# Check running PHP-FPM processes
ps aux | grep php-fpm | wc -l
# Should show 10+ processes initially
# During load test, monitor scaling
watch -n 1 "ps aux | grep php-fpm | wc -l"
# Should scale up to match traffic
```
---
## Additional Optimizations
### 1. Enable OpCache (PHP Bytecode Cache)
OpCache compiles PHP code once and caches it, avoiding recompilation on every request.
**Configuration** (`/etc/php/8.2/fpm/php.ini`):
```ini
opcache.enable = 1
opcache.memory_consumption = 256 ; 256MB for cache
opcache.max_accelerated_files = 20000 ; Cache up to 20k files
opcache.validate_timestamps = 0 ; Don't check for file changes (production)
opcache.revalidate_freq = 0
opcache.fast_shutdown = 1
```
**Expected Impact**: 30-50% faster PHP execution
### 2. Optimize Nginx (if applicable)
```nginx
worker_processes auto; # Use all CPU cores
worker_connections 1024; # Handle 1024 connections per worker
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript;
```
### 3. Increase MySQL Connections
If you see "too many connections" errors:
```bash
sudo nano /etc/mysql/my.cnf
[mysqld]
max_connections = 200
sudo systemctl restart mysql
```
### 4. Monitor Resource Usage
```bash
# CPU and memory usage
htop
# PHP-FPM status (if enabled)
curl http://localhost/status?full
# Disk I/O
iostat -x 1
# Network usage
iftop
```
---
## Testing Your Optimization
### Before and After Comparison
**Test Tool**: MaplePress CLI `wptest` command
```bash
cd native/desktop/maplepress-cli
# Run concurrent test (25 simultaneous requests)
go run main.go wptest --url https://your-site.com --mode parallel --concurrency 25
```
**Expected Results:**
| Metric | Before | After (Optimized) | Improvement |
|--------|--------|-------------------|-------------|
| Average Response | 8,000ms | 300-500ms | **16-27x faster** |
| P95 Latency | 9,000ms | < 1,000ms | **9x faster** |
| Concurrent Handling | Queues 20/25 | Handles all 25 | **No queuing** |
### In-Plugin Speed Test
Use the built-in **MaplePress → Speed Test** page in WordPress admin:
1. Go to **MaplePress** → **Speed Test**
2. Select number of trials (10-20 recommended)
3. Click **Run Speed Test**
4. Review results and recommendations
---
## Common Performance Issues
### Issue: High PHP Memory Usage
**Symptoms**:
- PHP workers crash with "out of memory"
- WordPress shows white screen/500 errors
**Solutions**:
```ini
# Reduce worker count
pm.max_children = 25 # Instead of 50
# Or increase PHP memory limit
memory_limit = 256M # In php.ini
```
### Issue: Still Slow After Optimization
**Check**:
1. **Database queries**: Slow plugins, missing indexes
```bash
# Enable MySQL slow query log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
```
2. **Caching**: Aggressive caching can hide PHP-FPM issues
- Clear all caches and test
- Disable page caching temporarily
3. **Network latency**: Test from different locations
```bash
curl -w "@curl-format.txt" -o /dev/null -s https://your-site.com
```
4. **Resource limits**: Check if hitting CPU/RAM limits
```bash
# Monitor during load test
top
free -h
```
---
## Production Deployment Checklist
- [ ] PHP-FPM `pm.max_children` increased (≥ 25 for 2GB, ≥ 50 for 4GB)
- [ ] OpCache enabled and configured
- [ ] Nginx worker_processes set to `auto`
- [ ] MySQL max_connections increased (≥ 200)
- [ ] Caching strategy reviewed (not hiding problems)
- [ ] Monitoring tools installed (htop, iotop, iftop)
- [ ] Load testing performed (wptest with concurrency 25+)
- [ ] Resource usage monitored under load
- [ ] Backup/rollback plan in place
---
## Additional Resources
### WordPress Documentation
- [PHP-FPM Configuration](https://www.php.net/manual/en/install.fpm.configuration.php)
- [OpCache Configuration](https://www.php.net/manual/en/opcache.configuration.php)
### MaplePress Resources
- **Production Deployment Guide**: See `cloud/infrastructure/production/setup/08_wordpress.md` in the MaplePress repository
- **MaplePress CLI**: Speed testing and diagnostics tool
- **Support**: Contact MaplePress support for optimization assistance
### Performance Tools
- **htop**: Interactive process viewer
- **iotop**: Disk I/O monitoring
- **iftop**: Network bandwidth monitoring
- **MySQL Tuner**: `mysqltuner.pl` for database optimization
---
## Summary
**The #1 performance issue is PHP-FPM worker starvation, not backend speed.**
MaplePress backend processes searches in < 20ms, but if your WordPress site only has 5 PHP-FPM workers and receives 25 concurrent requests, users will wait 8-10 seconds while requests queue.
**Quick Win**: Increase `pm.max_children` from 5 to 50 (on 4GB server) for **16-27x faster response times**.
Your MaplePress subscription includes a lightning-fast backend. Ensure your WordPress frontend can keep up! ⚡

View file

@ -0,0 +1,362 @@
<?php
/**
* Admin dashboard page template.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$options = get_option( 'maplepress_settings' );
$has_api_key = ! empty( $options['api_key'] );
$is_verified = isset( $options['is_verified'] ) && $options['is_verified'];
// Refresh quota data if verified and API credentials are available
if ( $is_verified && ! empty( $options['api_url'] ) && ! empty( $options['api_key'] ) ) {
require_once MAPLEPRESS_PLUGIN_DIR . 'includes/class-maplepress-api-client.php';
$api_client = new MaplePress_API_Client( $options['api_url'], $options['api_key'] );
$fresh_status = $api_client->verify_connection();
// Update options with fresh quota data if API call succeeded
if ( ! is_wp_error( $fresh_status ) ) {
if ( isset( $fresh_status['search_requests_count'] ) ) {
$options['search_requests_count'] = absint( $fresh_status['search_requests_count'] );
}
if ( isset( $fresh_status['storage_used_bytes'] ) ) {
$options['storage_used_bytes'] = absint( $fresh_status['storage_used_bytes'] );
}
if ( isset( $fresh_status['monthly_pages_indexed'] ) ) {
$options['monthly_pages_indexed'] = absint( $fresh_status['monthly_pages_indexed'] );
}
if ( isset( $fresh_status['total_pages_indexed'] ) ) {
$options['total_pages_indexed'] = absint( $fresh_status['total_pages_indexed'] );
}
}
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'MaplePress Dashboard', 'maplepress' ); ?></h1>
<?php
// Display sync status messages
if ( isset( $_GET['sync_status'] ) ) {
if ( $_GET['sync_status'] === 'success' && isset( $_GET['synced_count'] ) ) {
?>
<div class="notice notice-success is-dismissible">
<p>
<?php
printf(
/* translators: %d: number of pages synced */
esc_html__( '✓ Successfully synced %d pages to MaplePress!', 'maplepress' ),
absint( $_GET['synced_count'] )
);
?>
</p>
</div>
<?php
} elseif ( $_GET['sync_status'] === 'error' && isset( $_GET['sync_message'] ) ) {
?>
<div class="notice notice-error is-dismissible">
<p>
<?php
printf(
/* translators: %s: error message */
esc_html__( '✗ Sync failed: %s', 'maplepress' ),
esc_html( urldecode( $_GET['sync_message'] ) )
);
?>
</p>
</div>
<?php
} elseif ( $_GET['sync_status'] === 'no_posts' ) {
?>
<div class="notice notice-warning is-dismissible">
<p><?php esc_html_e( 'No published posts or pages found to sync.', 'maplepress' ); ?></p>
</div>
<?php
}
}
?>
<?php if ( ! $has_api_key ) : ?>
<!-- No API key - prompt setup -->
<div class="notice notice-warning" style="padding: 20px; margin: 20px 0;">
<h2 style="margin-top: 0;"><?php esc_html_e( 'Welcome to MaplePress!', 'maplepress' ); ?></h2>
<p><?php esc_html_e( 'You need to connect your site to MaplePress to use the cloud services.', 'maplepress' ); ?></p>
<p>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=maplepress-settings' ) ); ?>" class="button button-primary button-large">
<?php esc_html_e( 'Connect to MaplePress', 'maplepress' ); ?>
</a>
</p>
</div>
<?php elseif ( ! $is_verified ) : ?>
<!-- API key connected but NOT verified - show verification required notice -->
<div class="notice notice-error" style="padding: 20px; margin: 20px 0; border-left: 4px solid #dc3232;">
<h2 style="margin-top: 0; color: #dc3232;">
⚠️ <?php esc_html_e( 'Domain Verification Required', 'maplepress' ); ?>
</h2>
<p>
<?php esc_html_e( 'Your API key is connected, but your domain needs to be verified before you can use MaplePress features.', 'maplepress' ); ?>
</p>
<p>
<strong><?php esc_html_e( 'What you need to do:', 'maplepress' ); ?></strong>
</p>
<ol style="margin-left: 20px;">
<li><?php esc_html_e( 'Go to Settings → MaplePress', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Follow the DNS verification instructions', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Add the DNS TXT record to your domain', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Click "Verify Domain Now"', 'maplepress' ); ?></li>
</ol>
<p>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=maplepress-settings' ) ); ?>" class="button button-primary button-large">
<?php esc_html_e( 'Complete Verification', 'maplepress' ); ?>
</a>
</p>
<hr style="margin: 20px 0;">
<p style="margin: 0;">
<strong><?php esc_html_e( 'Features disabled until verification:', 'maplepress' ); ?></strong><br>
<span style="color: #666;">
<?php esc_html_e( 'Page syncing, search indexing, and all MaplePress cloud services are currently unavailable.', 'maplepress' ); ?>
</span>
</p>
</div>
<?php return; // Stop rendering dashboard content if not verified ?>
<?php else : ?>
<!-- Connected - show dashboard -->
<!-- Connection Status -->
<div class="notice notice-success" style="padding: 15px; margin: 20px 0; border-left-width: 4px;">
<h2 style="margin: 0; color: #46b450;">
<?php esc_html_e( 'Connected to MaplePress', 'maplepress' ); ?>
</h2>
<p style="margin: 10px 0 0 0;">
<strong><?php esc_html_e( 'Domain:', 'maplepress' ); ?></strong> <?php echo esc_html( $options['domain'] ?? 'N/A' ); ?>
&nbsp;|&nbsp;
<strong><?php esc_html_e( 'Billing:', 'maplepress' ); ?></strong> <?php esc_html_e( 'Usage-Based', 'maplepress' ); ?>
</p>
</div>
<!-- Usage-Based Billing Notice -->
<div class="notice notice-info" style="padding: 15px; margin: 20px 0;">
<h3 style="margin: 0 0 10px 0;"> <?php esc_html_e( 'Usage-Based Billing', 'maplepress' ); ?></h3>
<p><?php esc_html_e( 'MaplePress now uses usage-based billing with no limits or quotas. You pay only for what you use:', 'maplepress' ); ?></p>
<ul style="margin: 10px 0 10px 20px;">
<li><?php esc_html_e( '✓ No search request limits', 'maplepress' ); ?></li>
<li><?php esc_html_e( '✓ No page indexing limits', 'maplepress' ); ?></li>
<li><?php esc_html_e( '✓ No storage limits', 'maplepress' ); ?></li>
<li><?php esc_html_e( '✓ Generous rate limits for anti-abuse (10,000 requests/hour)', 'maplepress' ); ?></li>
</ul>
<p>
<a href="https://getmaplepress.com/pricing" target="_blank" class="button">
<?php esc_html_e( 'View Pricing Details', 'maplepress' ); ?>
</a>
</p>
</div>
<!-- Quick Stats -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 30px 0;">
<!-- Monthly Searches -->
<div class="postbox" style="padding: 20px;">
<h3 style="margin: 0 0 10px 0;"><?php esc_html_e( 'Monthly Searches', 'maplepress' ); ?></h3>
<p style="font-size: 32px; font-weight: bold; margin: 0; color: #2271b1;">
<?php echo esc_html( number_format( $options['search_requests_count'] ?? 0 ) ); ?>
</p>
<p style="margin: 5px 0 0 0; color: #666;">
<?php esc_html_e( 'this billing cycle', 'maplepress' ); ?>
</p>
</div>
<!-- Monthly Pages Indexed -->
<div class="postbox" style="padding: 20px;">
<h3 style="margin: 0 0 10px 0;"><?php esc_html_e( 'Monthly Indexing', 'maplepress' ); ?></h3>
<p style="font-size: 32px; font-weight: bold; margin: 0; color: #2271b1;">
<?php echo esc_html( number_format( $options['monthly_pages_indexed'] ?? 0 ) ); ?>
</p>
<p style="margin: 5px 0 0 0; color: #666;">
<?php esc_html_e( 'pages this cycle', 'maplepress' ); ?>
</p>
</div>
<!-- Total Pages Indexed -->
<div class="postbox" style="padding: 20px;">
<h3 style="margin: 0 0 10px 0;"><?php esc_html_e( 'Total Pages', 'maplepress' ); ?></h3>
<p style="font-size: 32px; font-weight: bold; margin: 0; color: #2271b1;">
<?php echo esc_html( number_format( $options['total_pages_indexed'] ?? 0 ) ); ?>
</p>
<p style="margin: 5px 0 0 0; color: #666;">
<?php esc_html_e( 'all-time indexed', 'maplepress' ); ?>
</p>
</div>
<!-- Storage Used -->
<div class="postbox" style="padding: 20px;">
<h3 style="margin: 0 0 10px 0;"><?php esc_html_e( 'Storage Used', 'maplepress' ); ?></h3>
<p style="font-size: 32px; font-weight: bold; margin: 0; color: #2271b1;">
<?php
$storage_mb = round( ( $options['storage_used_bytes'] ?? 0 ) / 1024 / 1024, 1 );
echo esc_html( $storage_mb );
?>
<span style="font-size: 16px;">MB</span>
</p>
<p style="margin: 5px 0 0 0; color: #666;">
<?php esc_html_e( 'cumulative usage', 'maplepress' ); ?>
</p>
</div>
</div>
<!-- Detailed Usage -->
<h2><?php esc_html_e( 'Usage Details', 'maplepress' ); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 40%;"><?php esc_html_e( 'Resource', 'maplepress' ); ?></th>
<th style="width: 30%;"><?php esc_html_e( 'Usage', 'maplepress' ); ?></th>
<th style="width: 30%;"><?php esc_html_e( 'Period', 'maplepress' ); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong><?php esc_html_e( 'Monthly Searches', 'maplepress' ); ?></strong></td>
<td><?php echo esc_html( number_format( $options['search_requests_count'] ?? 0 ) ); ?> requests</td>
<td><?php esc_html_e( 'Current billing cycle', 'maplepress' ); ?></td>
</tr>
<tr>
<td><strong><?php esc_html_e( 'Monthly Indexing', 'maplepress' ); ?></strong></td>
<td><?php echo esc_html( number_format( $options['monthly_pages_indexed'] ?? 0 ) ); ?> pages</td>
<td><?php esc_html_e( 'Current billing cycle', 'maplepress' ); ?></td>
</tr>
<tr>
<td><strong><?php esc_html_e( 'Total Pages Indexed', 'maplepress' ); ?></strong></td>
<td><?php echo esc_html( number_format( $options['total_pages_indexed'] ?? 0 ) ); ?> pages</td>
<td><?php esc_html_e( 'All-time cumulative', 'maplepress' ); ?></td>
</tr>
<tr>
<td><strong><?php esc_html_e( 'Storage Used', 'maplepress' ); ?></strong></td>
<td>
<?php
$used_mb = round( ( $options['storage_used_bytes'] ?? 0 ) / 1024 / 1024, 2 );
echo esc_html( $used_mb ) . ' MB';
?>
</td>
<td><?php esc_html_e( 'All-time cumulative', 'maplepress' ); ?></td>
</tr>
</tbody>
</table>
<!-- Quick Actions -->
<h2 style="margin-top: 40px;"><?php esc_html_e( 'Quick Actions', 'maplepress' ); ?></h2>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=maplepress-initial-sync' ) ); ?>" class="button button-primary button-large">
<?php esc_html_e( 'Sync All Pages Now', 'maplepress' ); ?>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=maplepress-settings' ) ); ?>" class="button button-large">
<?php esc_html_e( 'Settings', 'maplepress' ); ?>
</a>
<a href="https://getmaplepress.com/dashboard" target="_blank" class="button button-large">
<?php esc_html_e( '→ Open Cloud Dashboard', 'maplepress' ); ?>
</a>
</div>
<!-- Advanced Tools -->
<h2 style="margin-top: 40px;"><?php esc_html_e( 'Advanced', 'maplepress' ); ?></h2>
<div class="postbox" style="padding: 20px; margin-bottom: 20px;">
<h3 style="margin-top: 0;"><?php esc_html_e( 'Diagnostic Tools', 'maplepress' ); ?></h3>
<p>
<?php esc_html_e( 'Access advanced diagnostic and testing tools for your MaplePress integration.', 'maplepress' ); ?>
</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 30px;">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=maplepress-speedtest' ) ); ?>" class="button button-large">
<span class="dashicons dashicons-performance" style="vertical-align: middle;"></span>
<?php esc_html_e( 'Search Speed Test', 'maplepress' ); ?>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=maplepress-system-info' ) ); ?>" class="button button-large">
<span class="dashicons dashicons-info" style="vertical-align: middle;"></span>
<?php esc_html_e( 'System Info', 'maplepress' ); ?>
</a>
</div>
<hr style="margin: 20px 0;">
<h3 style="margin-top: 20px; color: #d63638;"><?php esc_html_e( 'Danger Zone', 'maplepress' ); ?></h3>
<p>
<?php esc_html_e( 'Permanently remove MaplePress plugin and all local data.', 'maplepress' ); ?>
</p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>" style="margin-top: 15px;">
<input type="hidden" name="action" value="maplepress_reset_and_delete">
<?php wp_nonce_field( 'maplepress_reset_and_delete' ); ?>
<button type="submit"
class="button button-large"
style="color: #ffffff; background-color: #d63638; border-color: #d63638;"
onmouseover="this.style.backgroundColor='#b32d2e';"
onmouseout="this.style.backgroundColor='#d63638';"
onclick="return confirm('<?php echo esc_js( __( '⚠️ WARNING: This action is PERMANENT and cannot be undone!\n\nThis will:\n✗ Delete all MaplePress settings from WordPress\n✗ Remove API keys and configuration\n✗ Clear verification status\n✗ Deactivate and DELETE the plugin\n\n⚠️ Your data in the MaplePress cloud will remain safe and can be accessed again if you reinstall.\n\nAre you absolutely sure you want to continue?', 'maplepress' ) ); ?>">
<span class="dashicons dashicons-trash" style="vertical-align: middle;"></span>
<?php esc_html_e( 'Reset & Delete Plugin', 'maplepress' ); ?>
</button>
</form>
<p class="description" style="color: #d63638; margin-top: 10px;">
<strong><?php esc_html_e( 'Warning:', 'maplepress' ); ?></strong>
<?php esc_html_e( 'This will permanently delete the plugin and all local settings. Your cloud data remains safe.', 'maplepress' ); ?>
</p>
</div>
<!-- Site Information -->
<h2><?php esc_html_e( 'Site Information', 'maplepress' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e( 'Domain', 'maplepress' ); ?></th>
<td><?php echo esc_html( $options['domain'] ?? 'N/A' ); ?></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Billing Model', 'maplepress' ); ?></th>
<td><strong><?php esc_html_e( 'Usage-Based', 'maplepress' ); ?></strong></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Status', 'maplepress' ); ?></th>
<td>
<?php if ( $options['enabled'] ) : ?>
<span style="color: #46b450; font-weight: bold;"> <?php esc_html_e( 'Active', 'maplepress' ); ?></span>
<?php else : ?>
<span style="color: #dc3232; font-weight: bold;"> <?php esc_html_e( 'Inactive', 'maplepress' ); ?></span>
<?php endif; ?>
</td>
</tr>
</table>
<?php endif; ?>
<!-- Footer -->
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #dcdcde; text-align: center; color: #646970; font-size: 13px;">
<p style="margin: 0;">
<?php
printf(
/* translators: %s: link to source code */
esc_html__( 'MaplePress is open-source software. %s', 'maplepress' ),
'<a href="https://codeberg.org/mapleopentech/monorepo/src/branch/main/native/wordpress/maplepress-plugin" target="_blank" rel="noopener noreferrer" style="color: #2271b1; text-decoration: none;">' .
esc_html__( 'View source code', 'maplepress' ) .
' <span class="dashicons dashicons-external" style="font-size: 12px; vertical-align: text-top;"></span></a>'
);
?>
&nbsp;|&nbsp;
<a href="https://getmaplepress.com" target="_blank" rel="noopener noreferrer" style="color: #2271b1; text-decoration: none;">
<?php esc_html_e( 'Documentation', 'maplepress' ); ?>
</a>
&nbsp;|&nbsp;
<a href="https://codeberg.org/mapleopentech/monorepo/issues" target="_blank" rel="noopener noreferrer" style="color: #2271b1; text-decoration: none;">
<?php esc_html_e( 'Report an issue', 'maplepress' ); ?>
</a>
</p>
</div>
</div>

View file

@ -0,0 +1,187 @@
<?php
/**
* Admin sync page template.
* This page is shown when syncing all pages to MaplePress (initial or manual sync).
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$options = get_option( 'maplepress_settings', array() );
?>
<div class="wrap">
<h1><?php esc_html_e( 'Syncing Pages', 'maplepress' ); ?></h1>
<div style="max-width: 800px; margin: 40px auto; text-align: center;">
<div style="background: #fff; border: 1px solid #dcdcde; padding: 40px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<!-- Loading animation -->
<div style="margin-bottom: 30px;">
<span class="spinner is-active" style="float: none; width: 40px; height: 40px; margin: 0 auto; background-size: 40px 40px;"></span>
</div>
<!-- Status heading -->
<h2 id="maplepress-sync-status" style="margin: 20px 0; color: #2271b1; font-size: 24px;">
<?php esc_html_e( 'Preparing to sync your content...', 'maplepress' ); ?>
</h2>
<!-- Details text -->
<p id="maplepress-sync-details" style="color: #666; font-size: 16px; margin: 15px 0;">
<?php esc_html_e( 'This will only take a moment.', 'maplepress' ); ?>
</p>
<!-- Progress bar -->
<div id="maplepress-sync-progressbar" style="margin: 30px 0;">
<div style="background: #f0f0f1; border-radius: 4px; height: 30px; position: relative; overflow: hidden;">
<div id="maplepress-sync-progressbar-fill" style="background: linear-gradient(90deg, #2271b1 0%, #135e96 100%); height: 100%; width: 0%; transition: width 0.5s ease;">
<span id="maplepress-sync-progressbar-text" style="color: #fff; font-size: 14px; font-weight: 600; line-height: 30px; display: block; text-align: center;"></span>
</div>
</div>
</div>
<!-- Batch information -->
<div id="maplepress-sync-batch-info" style="background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 4px; padding: 20px; margin: 20px 0; display: none;">
<div style="display: flex; justify-content: space-around; text-align: center;">
<div>
<div style="font-size: 28px; font-weight: 600; color: #2271b1;" id="maplepress-sync-pages-synced">0</div>
<div style="color: #666; font-size: 14px;"><?php esc_html_e( 'Pages Synced', 'maplepress' ); ?></div>
</div>
<div>
<div style="font-size: 28px; font-weight: 600; color: #2271b1;" id="maplepress-sync-current-batch">0</div>
<div style="color: #666; font-size: 14px;"><?php esc_html_e( 'Current Batch', 'maplepress' ); ?></div>
</div>
<div>
<div style="font-size: 28px; font-weight: 600; color: #2271b1;" id="maplepress-sync-total-batches">0</div>
<div style="color: #666; font-size: 14px;"><?php esc_html_e( 'Total Batches', 'maplepress' ); ?></div>
</div>
</div>
</div>
<!-- Important note -->
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin-top: 30px; text-align: left;">
<p style="margin: 0; color: #856404;">
<strong><?php esc_html_e( 'Please do not close this page.', 'maplepress' ); ?></strong><br>
<?php esc_html_e( 'We are syncing all your published content to MaplePress.', 'maplepress' ); ?>
</p>
</div>
</div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
var statusText = $('#maplepress-sync-status');
var detailsText = $('#maplepress-sync-details');
var progressBar = $('#maplepress-sync-progressbar-fill');
var progressText = $('#maplepress-sync-progressbar-text');
var batchInfo = $('#maplepress-sync-batch-info');
var pagesSynced = $('#maplepress-sync-pages-synced');
var currentBatch = $('#maplepress-sync-current-batch');
var totalBatches = $('#maplepress-sync-total-batches');
// Start the sync
function startSync() {
statusText.text('<?php esc_html_e( 'Counting pages to sync...', 'maplepress' ); ?>');
detailsText.text('<?php esc_html_e( 'Analyzing your content...', 'maplepress' ); ?>');
progressBar.css('width', '5%');
progressText.text('5%');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'maplepress_initial_sync',
nonce: '<?php echo wp_create_nonce( 'maplepress_initial_sync' ); ?>'
},
timeout: 600000, // 10 minute timeout
success: function(response) {
if (response.success) {
// Show completion
progressBar.css('width', '100%');
progressText.text('100%');
statusText.html('<span style="color: #008a00;">✓ ' + response.data.message + '</span>');
detailsText.html(response.data.details || '');
// Redirect to dashboard after 2 seconds
setTimeout(function() {
window.location.href = '<?php echo admin_url( 'admin.php?page=maplepress&sync_status=success&synced_count=' ); ?>' + (response.data.synced_count || 0);
}, 2000);
} else {
// Show error
statusText.html('<span style="color: #d63638;">✗ Sync failed</span>');
detailsText.html('<strong><?php esc_html_e( 'Error:', 'maplepress' ); ?></strong> ' + (response.data || '<?php esc_html_e( 'Unknown error', 'maplepress' ); ?>'));
// Show retry button
$('<div style="margin-top: 20px;"><a href="<?php echo admin_url( 'admin.php?page=maplepress-settings' ); ?>" class="button button-primary"><?php esc_html_e( 'Return to Settings', 'maplepress' ); ?></a></div>').insertAfter('#maplepress-sync-progressbar');
}
},
error: function(xhr, status, error) {
statusText.html('<span style="color: #d63638;">✗ Network error</span>');
detailsText.html('<strong><?php esc_html_e( 'Error:', 'maplepress' ); ?></strong> ' + error);
// Show retry button
$('<div style="margin-top: 20px;"><a href="<?php echo admin_url( 'admin.php?page=maplepress-settings' ); ?>" class="button button-primary"><?php esc_html_e( 'Return to Settings', 'maplepress' ); ?></a></div>').insertAfter('#maplepress-sync-progressbar');
},
xhr: function() {
var xhr = new window.XMLHttpRequest();
var progressSimulator;
var startTime = Date.now();
// Start progress simulation
progressSimulator = setInterval(function() {
var elapsed = (Date.now() - startTime) / 1000;
if (elapsed < 5) {
// 5-15% - Analyzing content
var progress = 5 + (elapsed / 5) * 10;
progressBar.css('width', progress + '%');
progressText.text(Math.round(progress) + '%');
statusText.text('<?php esc_html_e( 'Analyzing your content...', 'maplepress' ); ?>');
} else if (elapsed < 10) {
// 15-30% - Preparing batches
var progress = 15 + ((elapsed - 5) / 5) * 15;
progressBar.css('width', progress + '%');
progressText.text(Math.round(progress) + '%');
statusText.text('<?php esc_html_e( 'Preparing batches for sync...', 'maplepress' ); ?>');
batchInfo.show();
} else if (elapsed < 60) {
// 30-80% - Syncing batches
var progress = 30 + ((elapsed - 10) / 50) * 50;
progressBar.css('width', Math.min(progress, 80) + '%');
progressText.text(Math.round(Math.min(progress, 80)) + '%');
statusText.text('<?php esc_html_e( 'Syncing pages to MaplePress...', 'maplepress' ); ?>');
detailsText.html('<?php esc_html_e( 'Processing batches...', 'maplepress' ); ?><br><small><?php esc_html_e( 'This may take a moment for large sites.', 'maplepress' ); ?></small>');
// Simulate batch progress
var estimatedBatches = Math.ceil(elapsed / 3);
currentBatch.text(estimatedBatches);
totalBatches.text(estimatedBatches + Math.ceil((60 - elapsed) / 3));
pagesSynced.text(estimatedBatches * 950);
} else {
// 80-95% - Finalizing
var progress = 80 + ((elapsed - 60) / 60) * 15;
progressBar.css('width', Math.min(progress, 95) + '%');
progressText.text(Math.round(Math.min(progress, 95)) + '%');
statusText.text('<?php esc_html_e( 'Finalizing sync...', 'maplepress' ); ?>');
detailsText.html('<?php esc_html_e( 'Almost done!', 'maplepress' ); ?>');
}
}, 500);
xhr.addEventListener('loadend', function() {
clearInterval(progressSimulator);
});
return xhr;
}
});
}
// Auto-start sync when page loads
setTimeout(startSync, 500);
});
</script>

View file

@ -0,0 +1,74 @@
<?php
/**
* Admin ready-to-sync page template.
* This page is shown after first-time connection to prompt user to manually start sync.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$options = get_option( 'maplepress_settings', array() );
?>
<div class="wrap">
<h1><?php esc_html_e( 'MaplePress - Ready to Sync', 'maplepress' ); ?></h1>
<div style="max-width: 800px; margin: 40px auto;">
<div style="background: #fff; border: 1px solid #dcdcde; padding: 40px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<!-- Success icon -->
<div style="text-align: center; margin-bottom: 30px;">
<span style="display: inline-block; width: 80px; height: 80px; line-height: 80px; border-radius: 50%; background: #00a32a; color: #fff; font-size: 48px;"></span>
</div>
<!-- Main message -->
<h2 style="text-align: center; margin: 20px 0; color: #1d2327; font-size: 28px;">
<?php esc_html_e( 'Connection Successful!', 'maplepress' ); ?>
</h2>
<p style="text-align: center; color: #666; font-size: 18px; margin: 20px 0;">
<?php esc_html_e( 'Your site is now connected to MaplePress.', 'maplepress' ); ?>
</p>
<!-- Important notice -->
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 20px; margin: 30px 0; text-align: center;">
<p style="margin: 0; color: #856404; font-size: 16px; font-weight: 600;">
<?php esc_html_e( 'You must sync all your pages to continue.', 'maplepress' ); ?>
</p>
<p style="margin: 10px 0 0 0; color: #856404; font-size: 14px;">
<?php esc_html_e( 'This is a one-time process that will make all your published content searchable via MaplePress.', 'maplepress' ); ?>
</p>
</div>
<!-- Action buttons -->
<div style="text-align: center; margin: 40px 0 20px 0;">
<a href="<?php echo admin_url( 'admin.php?page=maplepress-initial-sync' ); ?>" class="button button-primary button-hero" style="font-size: 18px; padding: 12px 36px; height: auto;">
<?php esc_html_e( 'Start Sync Now', 'maplepress' ); ?>
</a>
</div>
<!-- Divider -->
<hr style="margin: 40px 0; border: none; border-top: 1px solid #dcdcde;">
<!-- Secondary actions -->
<div style="text-align: center;">
<p style="color: #666; font-size: 14px; margin-bottom: 15px;">
<?php esc_html_e( 'Need to start over?', 'maplepress' ); ?>
</p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to reset all MaplePress settings? This will disconnect your site from MaplePress.', 'maplepress' ) ); ?>');">
<?php wp_nonce_field( 'maplepress_reset_settings' ); ?>
<input type="hidden" name="action" value="maplepress_reset_settings">
<button type="submit" class="button button-link-delete" style="color: #d63638; text-decoration: none;">
<?php esc_html_e( 'Reset All Settings', 'maplepress' ); ?>
</button>
</form>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,332 @@
<?php
/**
* Admin settings page template.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
// Exit if accessed directly.
if (!defined("ABSPATH")) {
exit();
} ?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<?php
$options = get_option("maplepress_settings");
$has_api_key = !empty($options["api_key"]);
$is_verified = isset($options["is_verified"]) && $options["is_verified"];
// Refresh quota data if verified and API credentials are available
// Fetch fresh data on every page load without persisting to avoid triggering hooks
if (
$is_verified &&
!empty($options["api_url"]) &&
!empty($options["api_key"])
) {
require_once MAPLEPRESS_PLUGIN_DIR .
"includes/class-maplepress-api-client.php";
$api_client = new MaplePress_API_Client(
$options["api_url"],
$options["api_key"],
);
$fresh_status = $api_client->verify_connection();
// Update options with fresh quota data if API call succeeded
// Note: We only update the in-memory $options array, not the database
// This ensures fresh display without triggering validation hooks
if (!is_wp_error($fresh_status)) {
// Update quota fields in options for display only
if (isset($fresh_status["search_requests_count"])) {
$options["search_requests_count"] = absint(
$fresh_status["search_requests_count"],
);
}
if (isset($fresh_status["storage_used_bytes"])) {
$options["storage_used_bytes"] = absint(
$fresh_status["storage_used_bytes"],
);
}
if (isset($fresh_status["monthly_pages_indexed"])) {
$options["monthly_pages_indexed"] = absint(
$fresh_status["monthly_pages_indexed"],
);
}
if (isset($fresh_status["total_pages_indexed"])) {
$options["total_pages_indexed"] = absint(
$fresh_status["total_pages_indexed"],
);
}
// Do NOT call update_option() here - it could trigger validation/sync hooks
}
}
?>
<?php if (!$has_api_key): ?>
<!-- First-time setup instructions -->
<div class="notice notice-info" style="padding: 15px; margin: 20px 0;">
<h2 style="margin-top: 0;"><?php esc_html_e(
"🚀 Welcome to MaplePress!",
"maplepress",
); ?></h2>
<p><?php esc_html_e(
"MaplePress provides cloud services for your WordPress site - starting with advanced search, with more features coming soon (uploads, metrics, analytics, etc.).",
"maplepress",
); ?></p>
<p><strong><?php esc_html_e(
'✨ When you save settings with "Enable MaplePress search" checked, all your existing pages will automatically sync!',
"maplepress",
); ?></strong></p>
<p><?php esc_html_e("To get started, follow these steps:", "maplepress"); ?></p>
<ol style="margin-left: 20px;">
<li>
<strong><?php esc_html_e(
"Create a MaplePress account:",
"maplepress",
); ?></strong><br>
<a href="https://getmaplepress.com/register" target="_blank" class="button button-primary" style="margin-top: 5px;">
<?php esc_html_e("Sign Up at MaplePress.io", "maplepress"); ?>
</a>
<?php esc_html_e("or", "maplepress"); ?>
<a href="https://getmaplepress.com/login" target="_blank" class="button" style="margin-top: 5px;">
<?php esc_html_e("Login to Existing Account", "maplepress"); ?>
</a>
</li>
<li style="margin-top: 10px;">
<strong><?php esc_html_e(
"Create a site in your dashboard",
"maplepress",
); ?></strong><br>
<span style="color: #666;"><?php esc_html_e(
"After logging in, create a new site for this WordPress installation.",
"maplepress",
); ?></span>
</li>
<li style="margin-top: 10px;">
<strong><?php esc_html_e(
"Copy your API URL and API key",
"maplepress",
); ?></strong><br>
<span style="color: #666;">
<?php esc_html_e(
"Your API URL is https://getmaplepress.ca (or your custom backend URL). Your API key will be displayed once - make sure to copy both!",
"maplepress",
); ?>
</span>
</li>
<li style="margin-top: 10px;">
<strong><?php esc_html_e(
"Paste your API URL and API key below and save",
"maplepress",
); ?></strong>
</li>
</ol>
</div>
<?php endif; ?>
<?php settings_errors("maplepress_settings"); ?>
<?php // Display sync status messages
if (isset($_GET["sync_status"])) {
if (
$_GET["sync_status"] === "success" &&
isset($_GET["synced_count"])
) { ?>
<div class="notice notice-success is-dismissible">
<p>
<?php printf(
/* translators: %d: number of pages synced */
esc_html__(
"✓ Successfully synced %d pages to MaplePress!",
"maplepress",
),
absint($_GET["synced_count"]),
); ?>
</p>
</div>
<?php } elseif (
$_GET["sync_status"] === "error" &&
isset($_GET["sync_message"])
) { ?>
<div class="notice notice-error is-dismissible">
<p>
<?php printf(
/* translators: %s: error message */
esc_html__("✗ Sync failed: %s", "maplepress"),
esc_html(urldecode($_GET["sync_message"])),
); ?>
</p>
</div>
<?php } elseif ($_GET["sync_status"] === "no_posts") { ?>
<div class="notice notice-warning is-dismissible">
<p><?php esc_html_e(
"No published posts or pages found to sync.",
"maplepress",
); ?></p>
</div>
<?php }
} ?>
<form method="post" action="options.php">
<?php
settings_fields("maplepress_settings_group");
do_settings_sections("maplepress");
submit_button(__("Save Settings & Verify Connection", "maplepress"));
?>
</form>
<?php if ($is_verified): ?>
<!-- Connection status - show site details -->
<hr>
<h2><?php esc_html_e("✓ Connected to MaplePress", "maplepress"); ?></h2>
<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e("Domain", "maplepress"); ?></th>
<td><?php echo esc_html($options["domain"] ?? "N/A"); ?></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e("Plan", "maplepress"); ?></th>
<td><strong><?php echo esc_html(
ucfirst($options["plan_tier"] ?? "free"),
); ?></strong></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e("Status", "maplepress"); ?></th>
<td>
<?php if ($options["enabled"]): ?>
<span style="color: #46b450; font-weight: bold;"> <?php esc_html_e(
"Active",
"maplepress",
); ?></span>
<?php else: ?>
<span style="color: #dc3232; font-weight: bold;"> <?php esc_html_e(
"Inactive (Enable above to activate)",
"maplepress",
); ?></span>
<?php endif; ?>
</td>
</tr>
</table>
<?php if (
isset($options["storage_quota_bytes"]) ||
isset($options["search_quota_monthly"]) ||
isset($options["indexing_quota_pages"])
): ?>
<h3><?php esc_html_e("Usage & Quotas", "maplepress"); ?></h3>
<table class="form-table">
<?php if (isset($options["storage_quota_bytes"])): ?>
<tr>
<th scope="row"><?php esc_html_e("Storage", "maplepress"); ?></th>
<td>
<?php
$used = isset($options["storage_used_bytes"])
? $options["storage_used_bytes"]
: 0;
$quota = $options["storage_quota_bytes"];
$used_mb = round($used / 1024 / 1024, 2);
$quota_mb = round($quota / 1024 / 1024, 2);
$percent = $quota > 0 ? round(($used / $quota) * 100, 1) : 0;
?>
<strong><?php echo esc_html($used_mb); ?> MB</strong> / <?php echo esc_html(
$quota_mb,
); ?> MB
<span style="color: <?php echo $percent > 80
? "#dc3232"
: "#46b450"; ?>;">(<?php echo esc_html($percent); ?>% used)</span>
</td>
</tr>
<?php endif; ?>
<?php if (isset($options["search_quota_monthly"])): ?>
<tr>
<th scope="row"><?php esc_html_e("Monthly Searches", "maplepress"); ?></th>
<td>
<?php
$used = isset($options["search_requests_count"])
? $options["search_requests_count"]
: 0;
$quota = $options["search_quota_monthly"];
$percent = $quota > 0 ? round(($used / $quota) * 100, 1) : 0;
?>
<strong><?php echo esc_html(
number_format($used),
); ?></strong> / <?php echo esc_html(number_format($quota)); ?>
<span style="color: <?php echo $percent > 80
? "#dc3232"
: "#46b450"; ?>;">(<?php echo esc_html($percent); ?>% used)</span>
</td>
</tr>
<?php endif; ?>
<?php if (isset($options["indexing_quota_pages"])): ?>
<tr>
<th scope="row"><?php esc_html_e("Monthly Indexing", "maplepress"); ?></th>
<td>
<?php
$used = isset($options["monthly_pages_indexed"])
? $options["monthly_pages_indexed"]
: 0;
$quota = $options["indexing_quota_pages"];
$percent = $quota > 0 ? round(($used / $quota) * 100, 1) : 0;
?>
<strong><?php echo esc_html(
number_format($used),
); ?></strong> / <?php echo esc_html(number_format($quota)); ?> pages
<span style="color: <?php echo $percent > 80
? "#dc3232"
: "#46b450"; ?>;">(<?php echo esc_html(
$percent,
); ?>% used this month)</span>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e("Total Pages Indexed", "maplepress"); ?></th>
<td>
<?php $total = isset($options["total_pages_indexed"])
? $options["total_pages_indexed"]
: 0; ?>
<strong><?php echo esc_html(number_format($total)); ?></strong> pages (all-time)
</td>
</tr>
<?php endif; ?>
</table>
<?php endif; ?>
<hr>
<h2><?php esc_html_e("Sync Pages", "maplepress"); ?></h2>
<p><?php esc_html_e(
"Sync all your published posts and pages to MaplePress. New posts and pages are automatically synced when published.",
"maplepress",
); ?></p>
<form method="post" action="<?php echo esc_url(admin_url("admin.php")); ?>">
<input type="hidden" name="action" value="maplepress_bulk_sync">
<?php wp_nonce_field("maplepress_bulk_sync"); ?>
<p>
<button type="submit" class="button button-primary">
<?php esc_html_e("Sync All Pages Now", "maplepress"); ?>
</button>
</p>
</form>
<p>
<a href="https://getmaplepress.com/dashboard" target="_blank" class="button">
<?php esc_html_e("→ Open MaplePress Dashboard", "maplepress"); ?>
</a>
</p>
<?php endif; ?>
<hr>
<h2><?php esc_html_e("About MaplePress", "maplepress"); ?></h2>
<p>
<?php printf(
/* translators: %s: MaplePress website URL */
esc_html__(
"MaplePress is a cloud services platform for WordPress that offloads computationally intensive tasks to dedicated infrastructure. Currently featuring advanced search capabilities, with more services coming soon including uploads, metrics, and analytics. Visit %s to learn more.",
"maplepress",
),
'<a href="https://mapleopentech.io" target="_blank">https://mapleopentech.io</a>',
); ?>
</p>
</div>

View file

@ -0,0 +1,536 @@
<?php
/**
* Admin settings page template.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$options = get_option( 'maplepress_settings', array() );
$has_api_key = ! empty( $options['api_key'] );
$is_verified = isset( $options['is_verified'] ) && $options['is_verified'];
// Debug: Log current options
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'MaplePress Settings Page - api_url: ' . ( $options['api_url'] ?? 'NOT SET' ) );
error_log( 'MaplePress Settings Page - api_key: ' . ( isset( $options['api_key'] ) ? 'SET (' . strlen( $options['api_key'] ) . ' chars)' : 'NOT SET' ) );
error_log( 'MaplePress Settings Page - has_api_key: ' . ( $has_api_key ? 'true' : 'false' ) );
error_log( 'MaplePress Settings Page - is_verified: ' . ( $is_verified ? 'true' : 'false' ) );
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'MaplePress Settings', 'maplepress' ); ?></h1>
<?php
// WordPress automatically displays settings errors, no need to call settings_errors() manually
?>
<?php if ( ! $has_api_key ) : ?>
<!-- First-time setup instructions -->
<div class="notice notice-info" style="padding: 15px; margin: 20px 0;">
<h2 style="margin-top: 0;"><?php esc_html_e( '🚀 Getting Started', 'maplepress' ); ?></h2>
<p><?php esc_html_e( 'Connect your WordPress site to MaplePress cloud services to enable advanced search and more.', 'maplepress' ); ?></p>
<ol style="margin-left: 20px;">
<li>
<strong><?php esc_html_e( 'Create a MaplePress account:', 'maplepress' ); ?></strong><br>
<a href="https://getmaplepress.com/register" target="_blank" class="button button-primary" style="margin-top: 5px;">
<?php esc_html_e( 'Sign Up', 'maplepress' ); ?>
</a>
<?php esc_html_e( 'or', 'maplepress' ); ?>
<a href="https://getmaplepress.com/login" target="_blank" class="button" style="margin-top: 5px;">
<?php esc_html_e( 'Login', 'maplepress' ); ?>
</a>
</li>
<li style="margin-top: 10px;">
<strong><?php esc_html_e( 'Create a site in your dashboard', 'maplepress' ); ?></strong>
</li>
<li style="margin-top: 10px;">
<strong><?php esc_html_e( 'Copy your API credentials and paste them below', 'maplepress' ); ?></strong>
</li>
</ol>
</div>
<?php endif; ?>
<form method="post" action="options.php">
<?php settings_fields( 'maplepress_settings_group' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="api_url"><?php esc_html_e( 'API URL', 'maplepress' ); ?></label>
</th>
<td>
<?php if ( $is_verified ) : ?>
<!-- Show URL as read-only when verified -->
<input type="text"
id="api_url"
value="<?php echo esc_attr( $options['api_url'] ?? '' ); ?>"
class="regular-text"
disabled>
<input type="hidden"
name="maplepress_settings[api_url]"
value="<?php echo esc_attr( $options['api_url'] ?? '' ); ?>">
<p class="description">
<?php esc_html_e( 'Connected. To change, disconnect first.', 'maplepress' ); ?>
</p>
<?php else : ?>
<!-- Allow editing when not verified -->
<input type="text"
id="api_url"
name="maplepress_settings[api_url]"
value="<?php echo esc_attr( ! empty( $options['api_url'] ) ? $options['api_url'] : 'https://getmaplepress.ca' ); ?>"
class="regular-text"
placeholder="https://getmaplepress.ca">
<p class="description">
<?php esc_html_e( 'Your MaplePress API endpoint URL', 'maplepress' ); ?>
</p>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row">
<label for="api_key"><?php esc_html_e( 'API Key', 'maplepress' ); ?></label>
</th>
<td>
<?php if ( $is_verified ) : ?>
<!-- Show masked key when verified -->
<div style="margin-bottom: 10px;">
<code style="background: #f0f0f1; padding: 8px 12px; border-radius: 3px; font-size: 13px;">
<?php
$prefix = $options['api_key_prefix'] ?? 'mpsk';
$last_four = $options['api_key_last_four'] ?? '****';
echo esc_html( $prefix . '_' . str_repeat( '*', 28 ) . $last_four );
?>
</code>
</div>
<input type="hidden"
name="maplepress_settings[api_key]"
value="<?php echo esc_attr( $options['api_key'] ?? '' ); ?>">
<p class="description">
<?php esc_html_e( 'Your API key is securely stored. To change it, disconnect first.', 'maplepress' ); ?>
</p>
<?php else : ?>
<!-- Allow editing when not verified -->
<input type="password"
id="api_key"
name="maplepress_settings[api_key]"
value="<?php echo esc_attr( $options['api_key'] ?? '' ); ?>"
class="regular-text"
placeholder="live_sk_... or test_sk_...">
<p class="description">
<?php esc_html_e( 'Your MaplePress API key. Use "live_" prefix for production (https://getmaplepress.ca) or "test_" prefix for development/custom URLs.', 'maplepress' ); ?>
</p>
<?php endif; ?>
</td>
</tr>
<?php if ( $has_api_key && ! $is_verified ) : ?>
<!-- DNS Verification Required Section -->
<tr>
<th scope="row" colspan="2">
<h3 style="margin: 30px 0 10px 0; color: #d63638;">
⚠️ <?php esc_html_e( 'Domain Verification Required', 'maplepress' ); ?>
</h3>
</th>
</tr>
<tr>
<td colspan="2">
<div class="notice notice-warning inline" style="margin: 0; padding: 20px; background: #fff3cd; border-left: 4px solid #ffc107;">
<h4 style="margin-top: 0;">
<?php esc_html_e( '🔐 Your API key is connected, but your domain needs verification to enable syncing.', 'maplepress' ); ?>
</h4>
<p><?php esc_html_e( 'To verify ownership of your domain, please add the following DNS TXT record:', 'maplepress' ); ?></p>
<table class="widefat" style="background: #f9f9f9; margin: 15px 0; max-width: 600px;">
<tbody>
<tr>
<td style="width: 150px; font-weight: bold; padding: 12px;">
<?php esc_html_e( 'Host/Name:', 'maplepress' ); ?>
</td>
<td style="padding: 12px;">
<code style="background: #fff; padding: 5px 10px; font-size: 13px;">
<?php echo esc_html( $options['domain'] ?? 'your-domain.com' ); ?>
</code>
</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 12px;">
<?php esc_html_e( 'Type:', 'maplepress' ); ?>
</td>
<td style="padding: 12px;">
<code style="background: #fff; padding: 5px 10px; font-size: 13px;">TXT</code>
</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 12px;">
<?php esc_html_e( 'Value:', 'maplepress' ); ?>
</td>
<td style="padding: 12px;">
<code style="background: #fff; padding: 5px 10px; font-size: 13px; word-break: break-all;">
maplepress-verify=<?php echo esc_html( $options['verification_token'] ?? 'xxx' ); ?>
</code>
</td>
</tr>
</tbody>
</table>
<div style="background: #e7f3ff; border-left: 3px solid #2271b1; padding: 15px; margin: 20px 0;">
<h4 style="margin-top: 0;">
<?php esc_html_e( '📋 Step-by-Step Instructions:', 'maplepress' ); ?>
</h4>
<ol style="margin: 10px 0 10px 20px; line-height: 1.8;">
<li><?php esc_html_e( 'Log in to your domain registrar (GoDaddy, Namecheap, Cloudflare, etc.)', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Navigate to DNS settings for your domain', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Add a new TXT record with the values shown above', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Wait 5-10 minutes for DNS propagation (can take up to 48 hours)', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Click the "Verify Domain Now" button below', 'maplepress' ); ?></li>
</ol>
</div>
<!-- Progress indicator (hidden by default) -->
<div id="maplepress-verification-progress" style="display: none; background: #fff; border: 1px solid #dcdcde; padding: 20px; margin: 20px 0; border-radius: 4px;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<span class="spinner is-active" style="float: none; margin: 0 10px 0 0;"></span>
<strong id="maplepress-verification-status" style="font-size: 14px;">Verifying domain...</strong>
</div>
<div id="maplepress-verification-details" style="color: #666; font-size: 13px; margin-top: 10px;">
<!-- Progress details will appear here -->
</div>
<!-- Progress bar -->
<div id="maplepress-verification-progressbar" style="display: none; margin-top: 15px;">
<div style="background: #f0f0f1; border-radius: 4px; height: 24px; position: relative; overflow: hidden;">
<div id="maplepress-verification-progressbar-fill" style="background: linear-gradient(90deg, #2271b1 0%, #135e96 100%); height: 100%; width: 0%; transition: width 0.3s ease; display: flex; align-items: center; justify-content: center;">
<span id="maplepress-verification-progressbar-text" style="color: #fff; font-size: 12px; font-weight: 600; position: absolute; width: 100%; text-align: center; z-index: 2;"></span>
</div>
</div>
</div>
</div>
<form id="maplepress-verify-form" method="post" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>" style="margin-top: 20px;">
<input type="hidden" name="action" value="maplepress_verify_domain">
<?php wp_nonce_field( 'maplepress_verify_domain' ); ?>
<p>
<button type="submit" id="maplepress-verify-button" class="button button-primary button-large">
🔍 <?php esc_html_e( 'Verify Domain Now', 'maplepress' ); ?>
</button>
<span style="margin-left: 15px; color: #666;">
<?php esc_html_e( 'Make sure you\'ve added the DNS record and waited a few minutes before clicking.', 'maplepress' ); ?>
</span>
</p>
</form>
<script>
jQuery(document).ready(function($) {
$('#maplepress-verify-form').on('submit', function(e) {
e.preventDefault();
var button = $('#maplepress-verify-button');
var progressBox = $('#maplepress-verification-progress');
var statusText = $('#maplepress-verification-status');
var detailsBox = $('#maplepress-verification-details');
var progressBar = $('#maplepress-verification-progressbar');
var progressBarFill = $('#maplepress-verification-progressbar-fill');
var progressBarText = $('#maplepress-verification-progressbar-text');
// Disable button and show progress
button.prop('disabled', true);
button.text('⏳ Verifying...');
progressBox.show();
statusText.text('Step 1 of 2: Verifying domain ownership...');
detailsBox.html('Checking DNS TXT record...');
var startTime = Date.now();
// Make AJAX request
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'maplepress_verify_and_sync_ajax',
nonce: '<?php echo wp_create_nonce( 'maplepress_verify_and_sync_ajax' ); ?>'
},
timeout: 300000, // 5 minute timeout for large sites
xhr: function() {
var xhr = new window.XMLHttpRequest();
// Simulate progress during long operations
var progressSimulator = setInterval(function() {
var elapsed = (Date.now() - startTime) / 1000;
if (elapsed < 5) {
detailsBox.html('Checking DNS TXT record...');
} else if (elapsed < 10) {
statusText.text('Step 2 of 2: Syncing pages to MaplePress...');
detailsBox.html('Preparing pages for sync...');
progressBar.show();
progressBarFill.css('width', '10%');
progressBarText.text('10%');
} else if (elapsed < 30) {
var progress = Math.min(10 + ((elapsed - 10) / 20) * 60, 70);
detailsBox.html('Syncing pages to MaplePress cloud...<br><small>This may take a moment for large sites...</small>');
progressBarFill.css('width', progress + '%');
progressBarText.text(Math.round(progress) + '%');
} else {
var progress = Math.min(70 + ((elapsed - 30) / 30) * 25, 95);
detailsBox.html('Almost done, finalizing sync...<br><small>Processing batches...</small>');
progressBarFill.css('width', progress + '%');
progressBarText.text(Math.round(progress) + '%');
}
}, 1000);
xhr.addEventListener('loadend', function() {
clearInterval(progressSimulator);
});
return xhr;
},
success: function(response) {
if (response.success) {
// Show 100% complete
progressBar.show();
progressBarFill.css('width', '100%');
progressBarText.text('100%');
statusText.html('<span style="color: #008a00;">✓ ' + response.data.message + '</span>');
detailsBox.html(response.data.details || '');
// Reload page after 2 seconds
setTimeout(function() {
window.location.href = '<?php echo admin_url( 'admin.php?page=maplepress-settings&verification_status=success' ); ?>';
}, 2000);
} else {
progressBar.hide();
statusText.html('<span style="color: #d63638;">✗ Verification failed</span>');
detailsBox.html('<strong>Error:</strong> ' + (response.data || 'Unknown error'));
button.prop('disabled', false);
button.text('🔍 <?php esc_html_e( 'Verify Domain Now', 'maplepress' ); ?>');
}
},
error: function(xhr, status, error) {
progressBar.hide();
statusText.html('<span style="color: #d63638;">✗ Network error</span>');
detailsBox.html('<strong>Error:</strong> ' + error);
button.prop('disabled', false);
button.text('🔍 <?php esc_html_e( 'Verify Domain Now', 'maplepress' ); ?>');
}
});
});
});
</script>
<?php if ( isset( $_GET['verification_status'] ) && $_GET['verification_status'] === 'failed' ) : ?>
<div class="notice notice-error inline" style="margin-top: 20px; padding: 15px; background: #fee; border-left: 4px solid #dc3232;">
<h4 style="margin: 0 0 10px 0; color: #dc3232;">
<?php esc_html_e( 'Verification Failed', 'maplepress' ); ?>
</h4>
<p style="margin: 0 0 15px 0; font-size: 14px;">
<strong><?php esc_html_e( 'Error:', 'maplepress' ); ?></strong><br>
<?php
if ( isset( $_GET['verification_message'] ) ) {
echo esc_html( urldecode( $_GET['verification_message'] ) );
} else {
esc_html_e( 'Unknown verification error. Please check your settings and try again.', 'maplepress' );
}
?>
</p>
<?php
$error_message = isset( $_GET['verification_message'] ) ? urldecode( $_GET['verification_message'] ) : '';
// Show different troubleshooting tips based on error type
if ( strpos( $error_message, 'API URL' ) !== false || strpos( $error_message, 'API Key' ) !== false ) :
?>
<div style="background: #fff; padding: 15px; border-radius: 4px; margin-top: 15px;">
<h4 style="margin: 0 0 10px 0;"><?php esc_html_e( 'Configuration Issue:', 'maplepress' ); ?></h4>
<p style="margin: 0;"><?php esc_html_e( 'Please go back and save your API URL and API Key before attempting verification.', 'maplepress' ); ?></p>
</div>
<?php else : ?>
<div style="background: #fff; padding: 15px; border-radius: 4px; margin-top: 15px;">
<h4 style="margin: 0 0 10px 0;"><?php esc_html_e( 'Common Issues:', 'maplepress' ); ?></h4>
<ul style="margin: 0; padding-left: 20px; line-height: 1.8;">
<li><?php esc_html_e( 'DNS record hasn\'t propagated yet (usually takes 5-10 minutes, can take up to 48 hours)', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'TXT record value doesn\'t match exactly (check for typos or extra spaces)', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'TXT record added to wrong domain or subdomain', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Backend API connectivity issue (check if https://getmaplepress.ca is accessible)', 'maplepress' ); ?></li>
</ul>
</div>
<div style="background: #e7f3ff; border-left: 3px solid #2271b1; padding: 15px; margin-top: 15px;">
<p style="margin: 0; font-size: 14px;">
<strong>💡 <?php esc_html_e( 'Tip:', 'maplepress' ); ?></strong>
<?php esc_html_e( 'You can check if your DNS record is visible using online tools like "whatsmydns.net" or "dnschecker.org". Search for TXT records for your domain.', 'maplepress' ); ?>
</p>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</td>
</tr>
<?php endif; ?>
<?php if ( isset( $_GET['verification_status'] ) && $_GET['verification_status'] === 'success' ) : ?>
<tr>
<td colspan="2">
<div class="notice notice-success inline" style="margin: 20px 0; padding: 15px;">
<p style="margin: 0;">
<strong> <?php esc_html_e( 'Domain Verified Successfully!', 'maplepress' ); ?></strong><br>
<?php
if ( isset( $_GET['sync_status'] ) && $_GET['sync_status'] === 'success' && isset( $_GET['synced_count'] ) ) {
printf(
esc_html__( 'Your domain has been verified and %d pages have been synced to MaplePress!', 'maplepress' ),
absint( $_GET['synced_count'] )
);
} else {
esc_html_e( 'Your site is now fully connected to MaplePress. You can now sync pages and use all features.', 'maplepress' );
}
?>
</p>
</div>
</td>
</tr>
<?php endif; ?>
<?php if ( $is_verified ) : ?>
<!-- Connection Status -->
<tr>
<th scope="row"><?php esc_html_e( 'Connection Status', 'maplepress' ); ?></th>
<td>
<span style="color: #46b450; font-weight: bold;">
<?php esc_html_e( 'Connected', 'maplepress' ); ?>
</span>
<p class="description">
<?php esc_html_e( 'Your site is successfully connected to MaplePress.', 'maplepress' ); ?>
</p>
</td>
</tr>
<!-- Billing Information -->
<tr>
<th scope="row"><?php esc_html_e( 'Billing Model', 'maplepress' ); ?></th>
<td>
<strong><?php esc_html_e( 'Usage-Based', 'maplepress' ); ?></strong>
<p class="description">
<?php esc_html_e( 'Pay only for what you use with no limits or quotas.', 'maplepress' ); ?>
<a href="https://getmaplepress.com/pricing" target="_blank">
<?php esc_html_e( 'View pricing details', 'maplepress' ); ?>
</a>
</p>
</td>
</tr>
<?php endif; ?>
<!-- Enable MaplePress -->
<tr>
<th scope="row"><?php esc_html_e( 'Enable Services', 'maplepress' ); ?></th>
<td>
<fieldset>
<label>
<input type="checkbox"
name="maplepress_settings[enabled]"
value="1"
<?php checked( 1, $options['enabled'] ?? false ); ?>>
<strong><?php esc_html_e( 'Enable MaplePress', 'maplepress' ); ?></strong>
</label>
<p class="description">
<?php esc_html_e( 'Turn on MaplePress cloud services for this site.', 'maplepress' ); ?>
</p>
</fieldset>
</td>
</tr>
<!-- Search Settings -->
<tr>
<th scope="row"><?php esc_html_e( 'Search Settings', 'maplepress' ); ?></th>
<td>
<fieldset>
<label>
<input type="checkbox"
name="maplepress_settings[enable_frontend_search]"
value="1"
<?php checked( 1, $options['enable_frontend_search'] ?? true ); ?>>
<?php esc_html_e( 'Enable frontend search', 'maplepress' ); ?>
</label>
<br>
<label style="margin-top: 8px; display: inline-block;">
<input type="checkbox"
name="maplepress_settings[enable_admin_search]"
value="1"
<?php checked( 1, $options['enable_admin_search'] ?? false ); ?>>
<?php esc_html_e( 'Enable admin search', 'maplepress' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Choose where MaplePress search should be used.', 'maplepress' ); ?>
</p>
</fieldset>
</td>
</tr>
</table>
<?php if ( $is_verified ) : ?>
<!-- Show both Save and Disconnect buttons when connected -->
<p class="submit">
<?php submit_button( __( 'Save Settings', 'maplepress' ), 'primary', 'submit', false ); ?>
&nbsp;
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=maplepress-settings&action=disconnect' ), 'maplepress_disconnect' ) ); ?>"
class="button button-secondary"
onclick="return confirm('<?php esc_attr_e( 'Are you sure you want to disconnect from MaplePress? Your data will remain in the cloud.', 'maplepress' ); ?>');">
<?php esc_html_e( 'Disconnect', 'maplepress' ); ?>
</a>
</p>
<?php else : ?>
<!-- Show only Connect button when not connected -->
<?php submit_button( __( 'Connect to MaplePress', 'maplepress' ), 'primary', 'submit', true ); ?>
<?php endif; ?>
</form>
<!-- Reset Settings -->
<hr style="margin: 40px 0 20px 0;">
<h3><?php esc_html_e( 'Reset Settings', 'maplepress' ); ?></h3>
<p><?php esc_html_e( 'If you need to start over, you can reset all MaplePress settings.', 'maplepress' ); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>" style="margin-top: 20px;">
<input type="hidden" name="action" value="maplepress_reset_settings">
<?php wp_nonce_field( 'maplepress_reset_settings' ); ?>
<button type="submit"
class="button button-secondary"
style="color: #d63638; border-color: #d63638;"
onclick="return confirm('<?php esc_attr_e( 'Are you sure you want to reset all MaplePress settings?\n\nThis will:\n- Delete API URL and API Key\n- Clear verification status\n- Remove all configuration\n\nYour data in the MaplePress cloud will NOT be affected.\n\nYou will need to reconnect after reset.', 'maplepress' ); ?>');">
🔄 <?php esc_html_e( 'Reset All Settings', 'maplepress' ); ?>
</button>
<p class="description">
<?php esc_html_e( 'This will delete all plugin settings from WordPress. Your data in the MaplePress cloud will remain safe.', 'maplepress' ); ?>
</p>
</form>
<?php if ( $is_verified ) : ?>
<!-- Show connection info when connected -->
<hr style="margin: 40px 0;">
<h2><?php esc_html_e( 'Connection Details', 'maplepress' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e( 'Domain', 'maplepress' ); ?></th>
<td><?php echo esc_html( $options['domain'] ?? 'N/A' ); ?></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Connected Since', 'maplepress' ); ?></th>
<td>
<?php
// Assuming connection doesn't change often, we can use current time as reference
// In a real scenario, you'd store a 'connected_at' timestamp
esc_html_e( 'Active connection', 'maplepress' );
?>
</td>
</tr>
</table>
<?php endif; ?>
</div>

View file

@ -0,0 +1,243 @@
<?php
/**
* Simple Admin Speed Test Page
*
* @package MaplePress_SearchSpeedTest
*/
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'maplepress-searchspeedtest' ) );
}
?>
<div class="wrap mpss-wrap">
<!-- Header -->
<div style="margin: 30px 0 40px 0;">
<h1 style="font-size: 32px; font-weight: 300; margin: 0 0 10px 0; color: #1e1e1e;">
<span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: 600;">
<?php esc_html_e( 'How Fast is Your Search?', 'maplepress-searchspeedtest' ); ?>
</span>
</h1>
<p style="font-size: 16px; color: #666; margin: 0;">
<?php esc_html_e( 'Find out how quickly visitors can search your website', 'maplepress-searchspeedtest' ); ?>
</p>
</div>
<!-- Test Configuration -->
<div class="mpss-test-config">
<div class="mpss-card" style="max-width: 900px; background: linear-gradient(to bottom, #ffffff 0%, #f8f9fa 100%); border: none; box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08); border-radius: 16px; padding: 40px;">
<!-- Number of Queries Section -->
<div style="margin-bottom: 35px;">
<label for="mpss-query-count" style="display: block; font-size: 15px; font-weight: 600; margin-bottom: 8px; color: #1e1e1e;">
<span style="display: inline-block; width: 32px; height: 32px; line-height: 32px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 50%; margin-right: 10px; font-size: 14px;">1</span>
<?php esc_html_e( 'How thorough should the test be?', 'maplepress-searchspeedtest' ); ?>
</label>
<p style="font-size: 13px; color: #666; margin: 0 0 15px 42px; line-height: 1.5;">
<?php esc_html_e( 'More searches = more accurate results, but takes longer to complete', 'maplepress-searchspeedtest' ); ?>
</p>
<div style="background: white; padding: 20px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);">
<select id="mpss-query-count" name="query_count" style="width: 100%; padding: 12px 16px; font-size: 15px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<option value="10" selected>10 searches - Super quick (recommended to start) </option>
<option value="50">50 searches - Quick and accurate</option>
<option value="100">100 searches - Balanced (good choice)</option>
<option value="250">250 searches - Very thorough</option>
<option value="500">500 searches - Heavy load test</option>
<option value="1000">1,000 searches - Serious stress test</option>
<option value="2500">2,500 searches - Extreme testing</option>
<option value="5000">5,000 searches - Maximum stress 🔥</option>
<option value="10000">10,000 searches - Ultimate challenge 💪</option>
</select>
</div>
</div>
<!-- Execution Mode Section -->
<div style="margin-bottom: 35px;">
<label style="display: block; font-size: 15px; font-weight: 600; margin-bottom: 8px; color: #1e1e1e;">
<span style="display: inline-block; width: 32px; height: 32px; line-height: 32px; text-align: center; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; border-radius: 50%; margin-right: 10px; font-size: 14px;">2</span>
<?php esc_html_e( 'How should we test?', 'maplepress-searchspeedtest' ); ?>
</label>
<p style="font-size: 13px; color: #666; margin: 0 0 15px 42px; line-height: 1.5;">
<?php esc_html_e( 'Choose how the test simulates visitors searching your site', 'maplepress-searchspeedtest' ); ?>
</p>
<!-- Normal Mode Card -->
<label class="mpss-mode-card" for="mpss-mode-serial" style="display: block; margin-bottom: 15px; cursor: pointer;">
<input type="radio" name="execution_mode" id="mpss-mode-serial" value="serial" checked style="display: none;">
<div class="mpss-mode-card-inner" style="background: white; padding: 20px; border-radius: 12px; border: 2px solid #e0e0e0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); transition: all 0.3s;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 24px; margin-right: 12px;">👤</span>
<div>
<div style="font-size: 16px; font-weight: 600; color: #1e1e1e;">
<?php esc_html_e( 'Normal - One Person at a Time', 'maplepress-searchspeedtest' ); ?>
<span class="mpss-mode-badge" style="display: inline-block; margin-left: 8px; padding: 2px 8px; background: #e8f5e9; color: #2e7d32; font-size: 11px; font-weight: 600; border-radius: 4px; text-transform: uppercase;">Start Here</span>
</div>
<div style="font-size: 13px; color: #666; margin-top: 4px;">
<?php esc_html_e( 'Simulates one visitor searching at a time', 'maplepress-searchspeedtest' ); ?>
</div>
</div>
</div>
<div id="mpss-serial-explanation" style="margin-top: 12px; padding: 12px; background: linear-gradient(to right, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05)); border-left: 3px solid #667eea; border-radius: 6px; font-size: 13px; color: #666; line-height: 1.6;">
💡 <?php esc_html_e( 'Best for everyday testing. Shows how fast searches are under normal conditions.', 'maplepress-searchspeedtest' ); ?>
</div>
</div>
</label>
<!-- Stress Test Mode Card -->
<label class="mpss-mode-card" for="mpss-mode-parallel" style="display: block; cursor: pointer;">
<input type="radio" name="execution_mode" id="mpss-mode-parallel" value="parallel" style="display: none;">
<div class="mpss-mode-card-inner" style="background: white; padding: 20px; border-radius: 12px; border: 2px solid #e0e0e0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); transition: all 0.3s;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 24px; margin-right: 12px;">👥</span>
<div>
<div style="font-size: 16px; font-weight: 600; color: #1e1e1e;">
<?php esc_html_e( 'Stress Test - Many People at Once', 'maplepress-searchspeedtest' ); ?>
<span class="mpss-mode-badge" style="display: inline-block; margin-left: 8px; padding: 2px 8px; background: #fff3e0; color: #e65100; font-size: 11px; font-weight: 600; border-radius: 4px; text-transform: uppercase;">Advanced</span>
</div>
<div style="font-size: 13px; color: #666; margin-top: 4px;">
<?php esc_html_e( 'Simulates many visitors searching at the same time', 'maplepress-searchspeedtest' ); ?>
</div>
</div>
</div>
<div id="mpss-parallel-explanation" style="margin-top: 12px; padding: 12px; background: linear-gradient(to right, rgba(255, 152, 0, 0.05), rgba(255, 87, 34, 0.05)); border-left: 3px solid #ff9800; border-radius: 6px; font-size: 13px; color: #e65100; line-height: 1.6;">
⚠️ <?php esc_html_e( 'Tests your site under heavy load. May temporarily slow down your website.', 'maplepress-searchspeedtest' ); ?>
</div>
</div>
</label>
</div>
<!-- Run Button -->
<div style="text-align: center; margin-top: 40px;">
<button id="mpss-run-test" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 18px 56px; font-size: 17px; font-weight: 600; border-radius: 50px; cursor: pointer; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); transition: all 0.3s; display: inline-flex; align-items: center; gap: 10px;">
<span style="font-size: 22px;">🚀</span>
<?php esc_html_e( 'Start Test Now', 'maplepress-searchspeedtest' ); ?>
</button>
<p style="font-size: 12px; color: #999; margin-top: 15px;">
<?php esc_html_e( 'This won\'t affect your live website or visitors', 'maplepress-searchspeedtest' ); ?>
</p>
</div>
</div>
</div>
<!-- Progress -->
<div id="mpss-progress" class="mpss-progress" style="display: none;">
<div class="mpss-card" style="max-width: 900px; background: linear-gradient(to bottom, #ffffff 0%, #f8f9fa 100%); border: none; box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08); border-radius: 16px; padding: 40px;">
<div style="text-align: center;">
<div style="font-size: 48px; margin-bottom: 20px;"></div>
<h2 style="font-size: 24px; font-weight: 600; margin: 0 0 15px 0;">
<?php esc_html_e( 'Testing Your Search Speed...', 'maplepress-searchspeedtest' ); ?>
</h2>
<p id="mpss-progress-text" style="font-size: 15px; color: #666; margin-bottom: 10px;">
<?php esc_html_e( 'Please wait, this will only take a moment...', 'maplepress-searchspeedtest' ); ?>
</p>
<div id="mpss-progress-counter" style="font-size: 32px; font-weight: bold; color: #667eea; margin: 15px 0;">
<span id="mpss-completed-count">0</span> / <span id="mpss-total-count">0</span>
</div>
<p style="font-size: 13px; color: #999; margin-bottom: 5px;">searches completed</p>
<p id="mpss-time-estimate" style="font-size: 13px; color: #667eea; font-weight: 600; margin-bottom: 25px;">
<?php esc_html_e( 'Estimated time: calculating...', 'maplepress-searchspeedtest' ); ?>
</p>
</div>
<div class="mpss-progress-bar">
<div id="mpss-progress-fill" class="mpss-progress-fill" style="width: 0%;"></div>
</div>
<div style="text-align: center; margin-top: 20px;">
<button id="mpss-cancel-test" style="background: transparent; color: #dc3232; border: 2px solid #dc3232; padding: 10px 24px; font-size: 14px; font-weight: 600; border-radius: 50px; cursor: pointer; transition: all 0.3s;">
<span style="font-size: 16px;"></span> <?php esc_html_e( 'Cancel Test', 'maplepress-searchspeedtest' ); ?>
</button>
</div>
</div>
</div>
<!-- Results -->
<div id="mpss-results" class="mpss-results" style="display: none;">
<div class="mpss-card" style="max-width: 900px; background: linear-gradient(to bottom, #ffffff 0%, #f8f9fa 100%); border: none; box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08); border-radius: 16px; padding: 40px;">
<div style="text-align: center; margin-bottom: 30px;">
<div style="font-size: 64px; margin-bottom: 15px;"></div>
<h2 style="font-size: 28px; font-weight: 600; margin: 0 0 10px 0;">
<?php esc_html_e( 'Your Results Are Ready!', 'maplepress-searchspeedtest' ); ?>
</h2>
<p style="font-size: 15px; color: #666; margin: 0;">
<?php esc_html_e( 'Here\'s how your search performed', 'maplepress-searchspeedtest' ); ?>
</p>
</div>
<!-- Summary Stats -->
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px; margin: 30px auto; max-width: 800px;">
<div style="text-align: center; padding: 30px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);">
<div style="font-size: 48px; font-weight: bold; color: #fff; margin-bottom: 10px;" id="mpss-total-queries">-</div>
<div style="font-size: 14px; color: rgba(255, 255, 255, 0.9); text-transform: uppercase; letter-spacing: 1px; font-weight: 600;"><?php esc_html_e( 'Searches Tested', 'maplepress-searchspeedtest' ); ?></div>
</div>
<div style="text-align: center; padding: 30px 20px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; box-shadow: 0 4px 15px rgba(245, 87, 108, 0.4);">
<div style="font-size: 48px; font-weight: bold; color: #fff; margin-bottom: 10px;" id="mpss-total-time">-</div>
<div style="font-size: 14px; color: rgba(255, 255, 255, 0.9); text-transform: uppercase; letter-spacing: 1px; font-weight: 600;"><?php esc_html_e( 'Time Taken', 'maplepress-searchspeedtest' ); ?></div>
</div>
</div>
<!-- Quick Recommendations -->
<div id="mpss-recommendations" style="margin: 30px 0;"></div>
<!-- Chart -->
<div style="margin: 40px 0;">
<h3 style="font-size: 18px; font-weight: 600; margin: 0 0 20px 0; color: #1e1e1e;">
📊 <?php esc_html_e( 'Individual Search Times', 'maplepress-searchspeedtest' ); ?>
</h3>
<canvas id="mpss-chart"></canvas>
</div>
<!-- Actions -->
<div style="display: flex; gap: 15px; justify-content: center; margin-top: 40px; padding-top: 30px; border-top: 1px solid #e0e0e0;">
<button id="mpss-run-again" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 32px; font-size: 15px; font-weight: 600; border-radius: 50px; cursor: pointer; box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3); transition: all 0.3s; display: inline-flex; align-items: center; gap: 8px;">
<span style="font-size: 16px;">🔄</span>
<?php esc_html_e( 'Test Again', 'maplepress-searchspeedtest' ); ?>
</button>
<button id="mpss-share-results" style="background: #3b5998; color: white; border: none; padding: 12px 32px; font-size: 15px; font-weight: 600; border-radius: 50px; cursor: pointer; box-shadow: 0 2px 10px rgba(59, 89, 152, 0.3); transition: all 0.3s; display: inline-flex; align-items: center; gap: 8px;">
<span style="font-size: 16px;">📤</span>
<?php esc_html_e( 'Share Results', 'maplepress-searchspeedtest' ); ?>
</button>
<button id="mpss-export-results" style="background: white; color: #667eea; border: 2px solid #667eea; padding: 12px 32px; font-size: 15px; font-weight: 600; border-radius: 50px; cursor: pointer; transition: all 0.3s; display: inline-flex; align-items: center; gap: 8px;">
<span style="font-size: 16px;">📥</span>
<?php esc_html_e( 'Download Results', 'maplepress-searchspeedtest' ); ?>
</button>
</div>
</div>
</div>
<!-- Share Modal -->
<div id="mpss-share-modal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); z-index: 9999; align-items: center; justify-content: center;">
<div style="background: white; padding: 40px; border-radius: 20px; max-width: 600px; width: 90%; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); position: relative;">
<button id="mpss-close-share-modal" style="position: absolute; top: 15px; right: 15px; background: none; border: none; font-size: 24px; cursor: pointer; color: #999; transition: color 0.2s;" onmouseover="this.style.color='#333'" onmouseout="this.style.color='#999'"></button>
<h2 style="margin: 0 0 25px 0; font-size: 24px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">
📤 Share Your Results
</h2>
<div id="mpss-share-content" style="margin-bottom: 30px; text-align: center;">
<div id="mpss-share-graphic" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 16px; color: white; margin-bottom: 20px;">
<!-- Share graphic will be generated here -->
</div>
<p style="color: #666; font-size: 13px;">Click a button below to share your results</p>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
<button class="mpss-share-twitter" style="background: #1DA1F2; color: white; border: none; padding: 15px; font-size: 15px; font-weight: 600; border-radius: 12px; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 8px;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 15px rgba(29, 161, 242, 0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<span style="font-size: 18px;">🐦</span>
Twitter / X
</button>
<button class="mpss-share-facebook" style="background: #1877F2; color: white; border: none; padding: 15px; font-size: 15px; font-weight: 600; border-radius: 12px; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 8px;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 15px rgba(24, 119, 242, 0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<span style="font-size: 18px;">👥</span>
Facebook
</button>
<button class="mpss-share-linkedin" style="background: #0A66C2; color: white; border: none; padding: 15px; font-size: 15px; font-weight: 600; border-radius: 12px; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 8px;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 15px rgba(10, 102, 194, 0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<span style="font-size: 18px;">💼</span>
LinkedIn
</button>
<button class="mpss-share-copy" style="background: #10b981; color: white; border: none; padding: 15px; font-size: 15px; font-weight: 600; border-radius: 12px; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 8px;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 15px rgba(16, 185, 129, 0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<span style="font-size: 18px;">📋</span>
Copy Link
</button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,152 @@
<?php
/**
* Admin System Information Page
*
* @package MaplePress
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Load system info class
require_once MAPLEPRESS_PLUGIN_DIR . 'includes/class-maplepress-system-info.php';
// Get system information
$system_info = MaplePress_System_Info::get_all_info();
?>
<div class="wrap">
<h1><?php esc_html_e( 'System Information', 'maplepress' ); ?></h1>
<div class="notice notice-info" style="padding: 15px; margin: 20px 0;">
<h2 style="margin-top: 0;">
📊 <?php esc_html_e( 'System Diagnostics', 'maplepress' ); ?>
</h2>
<p>
<?php esc_html_e( 'This page shows detailed information about your server configuration, PHP-FPM workers, and system resources. Use this data to diagnose performance bottlenecks.', 'maplepress' ); ?>
</p>
<p>
<strong><?php esc_html_e( 'Key Metrics to Check:', 'maplepress' ); ?></strong>
</p>
<ul style="margin-left: 20px;">
<li><?php esc_html_e( 'PHP-FPM Max Children: Should be ≥50 for 4GB servers to handle concurrent requests', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'OpCache Status: Should be enabled for better PHP performance', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Memory Limits: PHP memory_limit should be 256M+', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Load Average: Should be below CPU count for healthy performance', 'maplepress' ); ?></li>
<li><?php esc_html_e( 'Database Connections: max_connections should be 200+ for high traffic', 'maplepress' ); ?></li>
</ul>
</div>
<?php
// Check for critical issues
$warnings = array();
// Check PHP-FPM max_children
if ( isset( $system_info['php_fpm']['max_children'] ) && is_numeric( $system_info['php_fpm']['max_children'] ) ) {
if ( $system_info['php_fpm']['max_children'] < 25 ) {
$warnings[] = sprintf(
__( '⚠️ PHP-FPM max_children is %d. Recommended: 50+ for 4GB servers. This is the #1 WordPress performance bottleneck.', 'maplepress' ),
$system_info['php_fpm']['max_children']
);
}
}
// Check OpCache
if ( ! $system_info['php']['opcache_enabled'] ) {
$warnings[] = __( '⚠️ OpCache is not enabled. Enable it for 30-50% faster PHP execution.', 'maplepress' );
}
// Check memory limit
$memory_limit = ini_get( 'memory_limit' );
if ( preg_match( '/^(\d+)/', $memory_limit, $matches ) ) {
if ( (int) $matches[1] < 128 ) {
$warnings[] = sprintf(
__( '⚠️ PHP memory_limit is %s. Recommended: 256M or higher.', 'maplepress' ),
$memory_limit
);
}
}
// Display warnings
if ( ! empty( $warnings ) ) :
?>
<div class="notice notice-warning" style="padding: 15px; margin: 20px 0; border-left-color: #ffc107;">
<h3 style="margin-top: 0; color: #856404;">
⚠️ <?php esc_html_e( 'Performance Warnings', 'maplepress' ); ?>
</h3>
<ul style="margin-left: 20px; color: #856404;">
<?php foreach ( $warnings as $warning ) : ?>
<li><?php echo esc_html( $warning ); ?></li>
<?php endforeach; ?>
</ul>
<p style="margin-bottom: 0;">
<a href="<?php echo esc_url( MAPLEPRESS_PLUGIN_URL . 'docs/PERFORMANCE_OPTIMIZATION.md' ); ?>"
class="button button-primary"
target="_blank"
style="background: #856404; border-color: #856404;">
📖 <?php esc_html_e( 'Read Performance Optimization Guide', 'maplepress' ); ?>
</a>
</p>
</div>
<?php else : ?>
<div class="notice notice-success" style="padding: 15px; margin: 20px 0;">
<p style="margin: 0;">
<strong> <?php esc_html_e( 'System configuration looks good!', 'maplepress' ); ?></strong>
</p>
</div>
<?php endif; ?>
<!-- System Information Tables -->
<?php echo MaplePress_System_Info::format_as_html( $system_info ); ?>
<!-- Export Button -->
<div style="margin-top: 30px;">
<button id="copy-system-info" class="button button-primary">
📋 <?php esc_html_e( 'Copy All Info to Clipboard', 'maplepress' ); ?>
</button>
<button id="download-system-info" class="button">
💾 <?php esc_html_e( 'Download as JSON', 'maplepress' ); ?>
</button>
</div>
<script>
jQuery(document).ready(function($) {
// Copy to clipboard
$('#copy-system-info').on('click', function() {
var systemInfo = <?php echo wp_json_encode( $system_info ); ?>;
var text = JSON.stringify(systemInfo, null, 2);
navigator.clipboard.writeText(text).then(function() {
alert('<?php esc_html_e( 'System information copied to clipboard!', 'maplepress' ); ?>');
}).catch(function() {
// Fallback for older browsers
var textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert('<?php esc_html_e( 'System information copied to clipboard!', 'maplepress' ); ?>');
});
});
// Download as JSON
$('#download-system-info').on('click', function() {
var systemInfo = <?php echo wp_json_encode( $system_info ); ?>;
var dataStr = JSON.stringify(systemInfo, null, 2);
var dataBlob = new Blob([dataStr], {type: 'application/json'});
var url = URL.createObjectURL(dataBlob);
var link = document.createElement('a');
link.href = url;
link.download = 'maplepress-system-info-' + Date.now() + '.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
});
</script>
</div>

View file

@ -0,0 +1,41 @@
<?php
/**
* Fired during plugin activation.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
class MaplePress_Activator {
/**
* Activation tasks.
*
* Creates default options and flushes rewrite rules.
*/
public static function activate() {
// Set default options.
// Allow developers to override API URL via wp-config.php constant
$default_api_url = defined( 'MAPLEPRESS_API_URL' ) ? MAPLEPRESS_API_URL : '';
$default_options = array(
'api_url' => $default_api_url, // Empty by default - user must configure
'api_key' => '',
'site_id' => '',
'tenant_id' => '',
'domain' => '',
'plan_tier' => '',
'is_verified' => false,
'enabled' => false,
'needs_setup' => true, // Flag to show setup notice
);
add_option( 'maplepress_settings', $default_options );
// Set activation redirect flag
set_transient( 'maplepress_activation_redirect', true, 30 );
// Flush rewrite rules.
flush_rewrite_rules();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,226 @@
<?php
/**
* MaplePress API Client - handles communication with MaplePress backend.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
class MaplePress_API_Client {
/**
* API base URL.
*
* @var string
*/
private $api_url;
/**
* API key.
*
* @var string
*/
private $api_key;
/**
* Constructor.
*
* @param string $api_url API base URL.
* @param string $api_key API key.
*/
public function __construct( $api_url, $api_key ) {
$this->api_url = trailingslashit( $api_url );
$this->api_key = $api_key;
}
/**
* Verify API connection and API key.
*
* @return array|WP_Error Response data or WP_Error on failure.
*/
public function verify_connection() {
return $this->request( 'GET', 'api/v1/plugin/status' );
}
/**
* Trigger domain verification via DNS TXT record check.
*
* @return array|WP_Error Response data or WP_Error on failure.
*/
public function verify_domain() {
error_log( 'MaplePress API: verify_domain() called' );
$result = $this->request( 'POST', 'api/v1/plugin/verify' );
error_log( 'MaplePress API: verify_domain() result = ' . print_r( $result, true ) );
return $result;
}
/**
* Send a request to the MaplePress API.
*
* @param string $method HTTP method (GET, POST, PUT, DELETE).
* @param string $endpoint API endpoint (relative to base URL).
* @param array $body Optional. Request body data.
* @return array|WP_Error Response data or WP_Error on failure.
*/
private function request( $method, $endpoint, $body = array() ) {
$url = $this->api_url . ltrim( $endpoint, '/' );
// Debug logging (only enable when WP_DEBUG is true)
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "MaplePress API: Making $method request to: $url" );
}
$args = array(
'method' => $method,
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
),
'timeout' => 15,
);
if ( ! empty( $body ) && in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) ) {
$args['body'] = wp_json_encode( $body );
}
$response = wp_remote_request( $url, $args );
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( $status_code >= 400 ) {
$error_message = isset( $data['message'] ) ? $data['message'] : __( 'Unknown API error', 'maplepress' );
// Log full response for debugging
error_log( 'MaplePress API Error Response:' );
error_log( 'Status Code: ' . $status_code );
error_log( 'Raw Body: ' . $body );
error_log( 'Parsed Data: ' . print_r( $data, true ) );
// Determine error code based on status
$error_code = 'maplepress_api_error';
if ( $status_code === 429 ) {
$error_code = 'maplepress_rate_limited';
} elseif ( $status_code === 403 ) {
$error_code = 'maplepress_forbidden';
} elseif ( $status_code === 404 ) {
$error_code = 'maplepress_not_found';
}
return new WP_Error(
$error_code,
sprintf(
/* translators: 1: HTTP status code, 2: error message */
__( 'API Error (%1$d): %2$s', 'maplepress' ),
$status_code,
$error_message
),
array( 'status' => $status_code )
);
}
return $data;
}
/**
* Sync pages to MaplePress backend.
*
* @param array $pages Array of page data to sync.
* @return array|WP_Error Response data or WP_Error on failure.
*/
public function sync_pages( $pages ) {
$data = array(
'pages' => $pages,
);
return $this->request( 'POST', 'api/v1/plugin/pages/sync', $data );
}
/**
* Index a post in MaplePress (single page).
*
* @param int $post_id Post ID.
* @return array|WP_Error Response data or WP_Error on failure.
*/
public function index_post( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'invalid_post', __( 'Post not found', 'maplepress' ) );
}
// Format page data for API
$page_data = $this->format_page_data( $post );
// Sync single page
return $this->sync_pages( array( $page_data ) );
}
/**
* Format post/page data for API sync.
*
* @param WP_Post $post Post object.
* @return array Formatted page data.
*/
private function format_page_data( $post ) {
// Format timestamps in RFC3339 format (ISO 8601)
// Use actual GMT time if available, otherwise use server time converted to GMT
$published_at = '';
if ( $post->post_date_gmt && $post->post_date_gmt !== '0000-00-00 00:00:00' ) {
$published_at = gmdate( 'Y-m-d\TH:i:s\Z', strtotime( $post->post_date_gmt . ' UTC' ) );
}
$modified_at = '';
if ( $post->post_modified_gmt && $post->post_modified_gmt !== '0000-00-00 00:00:00' ) {
$modified_at = gmdate( 'Y-m-d\TH:i:s\Z', strtotime( $post->post_modified_gmt . ' UTC' ) );
}
return array(
'page_id' => (string) $post->ID,
'title' => $post->post_title,
'content' => wp_strip_all_tags( $post->post_content ),
'excerpt' => ! empty( $post->post_excerpt ) ? $post->post_excerpt : wp_trim_words( $post->post_content, 55, '...' ),
'url' => get_permalink( $post->ID ),
'status' => $post->post_status,
'post_type' => $post->post_type,
'author' => get_the_author_meta( 'display_name', $post->post_author ),
'published_at' => $published_at,
'modified_at' => $modified_at,
);
}
/**
* Remove a post from MaplePress index.
*
* @param int $post_id Post ID.
* @return array|WP_Error Response data or WP_Error on failure.
*/
public function delete_post( $post_id ) {
return $this->request( 'DELETE', 'api/v1/plugin/index/' . $post_id );
}
/**
* Search using MaplePress API.
*
* @param string $query Search query.
* @param array $args Optional. Additional search parameters.
* @return array|WP_Error Search results or WP_Error on failure.
*/
public function search( $query, $args = array() ) {
$params = array_merge(
array(
'query' => $query,
'limit' => 10,
'offset' => 0,
),
$args
);
return $this->request( 'POST', 'api/v1/plugin/pages/search', $params );
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Fired during plugin deactivation.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
class MaplePress_Deactivator {
/**
* Deactivation tasks.
*
* Flushes rewrite rules.
*/
public static function deactivate() {
// Flush rewrite rules.
flush_rewrite_rules();
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Register all actions and filters for the plugin.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
class MaplePress_Loader {
/**
* The array of actions registered with WordPress.
*
* @var array
*/
protected $actions;
/**
* The array of filters registered with WordPress.
*
* @var array
*/
protected $filters;
/**
* Initialize the collections used to maintain the actions and filters.
*/
public function __construct() {
$this->actions = array();
$this->filters = array();
}
/**
* Add a new action to the collection to be registered with WordPress.
*
* @param string $hook The name of the WordPress action.
* @param object $component A reference to the instance of the object.
* @param string $callback The name of the function definition on the $component.
* @param int $priority Optional. The priority at which the function should be fired. Default is 10.
* @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
*/
public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
$this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
}
/**
* Add a new filter to the collection to be registered with WordPress.
*
* @param string $hook The name of the WordPress filter.
* @param object $component A reference to the instance of the object.
* @param string $callback The name of the function definition on the $component.
* @param int $priority Optional. The priority at which the function should be fired. Default is 10.
* @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
*/
public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
$this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
}
/**
* A utility function that is used to register the actions and hooks into a single collection.
*
* @param array $hooks The collection of hooks that is being registered.
* @param string $hook The name of the WordPress filter/action.
* @param object $component A reference to the instance of the object.
* @param string $callback The name of the function definition on the $component.
* @param int $priority The priority at which the function should be fired.
* @param int $accepted_args The number of arguments that should be passed to the $callback.
* @return array
*/
private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
$hooks[] = array(
'hook' => $hook,
'component' => $component,
'callback' => $callback,
'priority' => $priority,
'accepted_args' => $accepted_args,
);
return $hooks;
}
/**
* Register the filters and actions with WordPress.
*/
public function run() {
foreach ( $this->filters as $hook ) {
add_filter( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] );
}
foreach ( $this->actions as $hook ) {
add_action( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] );
}
}
}

View file

@ -0,0 +1,252 @@
<?php
/**
* The public-facing functionality of the plugin.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
class MaplePress_Public {
/**
* The ID of this plugin.
*
* @var string
*/
private $plugin_name;
/**
* The version of this plugin.
*
* @var string
*/
private $version;
/**
* Initialize the class and set its properties.
*
* @param string $plugin_name The name of the plugin.
* @param string $version The version of this plugin.
*/
public function __construct( $plugin_name, $version ) {
$this->plugin_name = $plugin_name;
$this->version = $version;
}
/**
* Register the stylesheets for the public-facing side of the site.
*/
public function enqueue_styles() {
wp_enqueue_style(
$this->plugin_name,
MAPLEPRESS_PLUGIN_URL . 'assets/css/maplepress-public.css',
array(),
$this->version,
'all'
);
}
/**
* Register the JavaScript for the public-facing side of the site.
*/
public function enqueue_scripts() {
wp_enqueue_script(
$this->plugin_name,
MAPLEPRESS_PLUGIN_URL . 'assets/js/maplepress-public.js',
array( 'jquery' ),
$this->version,
false
);
}
/**
* Intercept WordPress search and use MaplePress search instead.
*
* @param WP_Query $query The WordPress query object.
*/
public function intercept_search( $query ) {
// Debug logging - detect if this is from CLI test
$is_cli_test = isset( $_SERVER['HTTP_X_MAPLEPRESS_TEST'] );
// Log every invocation when WP_DEBUG is on or when it's a CLI test
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( '=== MaplePress intercept_search called ===' );
error_log( 'is_main_query: ' . ( $query->is_main_query() ? 'true' : 'false' ) );
error_log( 'is_search: ' . ( $query->is_search() ? 'true' : 'false' ) );
error_log( 's param: ' . var_export( $query->get( 's' ), true ) );
error_log( 'CLI test header: ' . ( $is_cli_test ? 'true' : 'false' ) );
}
// Only intercept main search queries
// Check both is_search() and presence of 's' parameter to catch all search scenarios
$is_search_query = $query->is_search() || ! empty( $query->get( 's' ) );
if ( ! $query->is_main_query() || ! $is_search_query ) {
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Early return - not main query or not search' );
}
return;
}
// Check if MaplePress is enabled
$options = get_option( 'maplepress_settings' );
if ( empty( $options['enabled'] ) || empty( $options['api_url'] ) || empty( $options['api_key'] ) ) {
// MaplePress not enabled or configured, use default WordPress search
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Early return - not enabled or not configured' );
error_log( 'enabled: ' . var_export( $options['enabled'] ?? null, true ) );
error_log( 'api_url: ' . var_export( $options['api_url'] ?? null, true ) );
error_log( 'api_key present: ' . ( ! empty( $options['api_key'] ) ? 'yes' : 'no' ) );
}
return;
}
// Check if connection is verified
if ( empty( $options['is_verified'] ) ) {
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Early return - connection not verified' );
}
return;
}
// Check if search is enabled for the current context
// Speed test queries always proceed regardless of admin/frontend settings
if ( ! $is_speed_test ) {
$is_admin_area = is_admin();
$frontend_search_enabled = isset( $options['enable_frontend_search'] ) ? $options['enable_frontend_search'] : true;
$admin_search_enabled = isset( $options['enable_admin_search'] ) ? $options['enable_admin_search'] : false;
// Skip if admin search is not enabled and we're in admin
if ( $is_admin_area && ! $admin_search_enabled ) {
// In admin but admin search not enabled
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Early return - admin search not enabled' );
}
return;
}
// Skip if frontend search is not enabled and we're on frontend
if ( ! $is_admin_area && ! $frontend_search_enabled ) {
// On frontend but frontend search not enabled
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Early return - frontend search not enabled' );
}
return;
}
} else {
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Speed test query - bypassing admin/frontend check' );
}
}
// Get the search query
// Try get_search_query() first, then fall back to direct query parameter
$search_query = get_search_query();
if ( empty( $search_query ) ) {
$search_query = $query->get( 's' );
}
if ( empty( $search_query ) ) {
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Early return - empty search query' );
}
return;
}
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Proceeding with search for: ' . $search_query );
}
// Call MaplePress search API
require_once MAPLEPRESS_PLUGIN_DIR . 'includes/class-maplepress-api-client.php';
$api_client = new MaplePress_API_Client( $options['api_url'], $options['api_key'] );
$search_args = array(
'limit' => (int) $query->get( 'posts_per_page', 10 ),
'offset' => (int) ( ( $query->get( 'paged', 1 ) - 1 ) * $query->get( 'posts_per_page', 10 ) ),
);
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: Calling API with query: ' . $search_query );
error_log( 'MaplePress: API URL: ' . $options['api_url'] );
error_log( 'MaplePress: Search args: ' . json_encode( $search_args ) );
}
$result = $api_client->search( $search_query, $search_args );
if ( is_wp_error( $result ) ) {
// API error - fall back to default WordPress search
// Log the error for debugging
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'MaplePress search error: ' . $result->get_error_message() );
}
if ( $is_cli_test ) {
error_log( 'MaplePress search error (CLI test): ' . $result->get_error_message() );
}
return;
}
if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || $is_cli_test ) {
error_log( 'MaplePress: API call successful, results: ' . json_encode( $result ) );
}
// Extract page IDs from MaplePress results
$page_ids = array();
if ( ! empty( $result['hits'] ) ) {
foreach ( $result['hits'] as $result_item ) {
// Meilisearch returns 'id' field, not 'page_id'
if ( ! empty( $result_item['id'] ) ) {
$page_ids[] = (int) $result_item['id'];
}
}
}
// If no results, show empty results
if ( empty( $page_ids ) ) {
$query->set( 'post__in', array( 0 ) ); // Force no results
} else {
// Set query to only return posts matching MaplePress results in the correct order
$query->set( 'post__in', $page_ids );
$query->set( 'orderby', 'post__in' ); // Maintain MaplePress result order
// In admin context, respect the existing post_type filter (e.g., 'page' when in Pages list)
// On frontend, allow any post type
if ( ! is_admin() ) {
$query->set( 'post_type', 'any' ); // Allow any post type on frontend
}
// Note: In admin, we preserve whatever post_type was already set by WordPress
$query->set( 'post_status', 'publish' );
// Disable the default search query
$query->set( 's', '' );
}
// Store the original search query for display
$query->set( 'maplepress_search_query', $search_query );
// Store total results for pagination
if ( ! empty( $result['total_hits'] ) ) {
add_filter( 'found_posts', function( $found_posts, $query ) use ( $result ) {
if ( $query->get( 'maplepress_search_query' ) ) {
return (int) $result['total_hits'];
}
return $found_posts;
}, 10, 2 );
}
}
/**
* Restore the search query for display purposes.
*
* @param string $search_query The search query.
* @return string
*/
public function restore_search_query( $search_query ) {
global $wp_query;
$maplepress_query = $wp_query->get( 'maplepress_search_query' );
if ( ! empty( $maplepress_query ) ) {
return $maplepress_query;
}
return $search_query;
}
}

View file

@ -0,0 +1,454 @@
<?php
/**
* System Information Collector
*
* Collects detailed system information for diagnosing performance bottlenecks.
*
* @package MaplePress
*/
class MaplePress_System_Info {
/**
* Get all system information.
*
* @return array System information array.
*/
public static function get_all_info() {
return array(
'php' => self::get_php_info(),
'php_fpm' => self::get_php_fpm_info(),
'server' => self::get_server_info(),
'wordpress' => self::get_wordpress_info(),
'database' => self::get_database_info(),
'performance' => self::get_performance_info(),
);
}
/**
* Get PHP configuration information.
*
* @return array PHP configuration.
*/
private static function get_php_info() {
return array(
'version' => PHP_VERSION,
'sapi' => php_sapi_name(),
'memory_limit' => ini_get( 'memory_limit' ),
'max_execution_time' => ini_get( 'max_execution_time' ),
'max_input_time' => ini_get( 'max_input_time' ),
'post_max_size' => ini_get( 'post_max_size' ),
'upload_max_filesize' => ini_get( 'upload_max_filesize' ),
'max_input_vars' => ini_get( 'max_input_vars' ),
'display_errors' => ini_get( 'display_errors' ),
'error_reporting' => ini_get( 'error_reporting' ),
'opcache_enabled' => function_exists( 'opcache_get_status' ) && opcache_get_status() !== false,
'opcache_status' => function_exists( 'opcache_get_status' ) ? opcache_get_status() : null,
);
}
/**
* Get PHP-FPM configuration information.
*
* @return array PHP-FPM configuration.
*/
private static function get_php_fpm_info() {
$info = array(
'is_fpm' => false,
'pool_name' => 'N/A',
'pm_type' => 'N/A',
'max_children' => 'N/A',
'start_servers' => 'N/A',
'min_spare_servers' => 'N/A',
'max_spare_servers' => 'N/A',
'max_requests' => 'N/A',
'status_url' => 'N/A',
'active_processes' => 'N/A',
'total_processes' => 'N/A',
'idle_processes' => 'N/A',
'listen_queue' => 'N/A',
'listen_queue_len' => 'N/A',
'config_file' => 'N/A',
);
// Check if running under PHP-FPM
if ( php_sapi_name() === 'fpm-fcgi' || php_sapi_name() === 'cgi-fcgi' ) {
$info['is_fpm'] = true;
// Try to find PHP-FPM config file
$possible_config_paths = array(
'/etc/php/' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '/fpm/pool.d/www.conf',
'/usr/local/etc/php-fpm.d/www.conf',
'/etc/php-fpm.d/www.conf',
'/etc/php/fpm/pool.d/www.conf',
);
foreach ( $possible_config_paths as $path ) {
if ( file_exists( $path ) && is_readable( $path ) ) {
$info['config_file'] = $path;
$config = self::parse_php_fpm_config( $path );
if ( $config ) {
$info = array_merge( $info, $config );
}
break;
}
}
// Try to get PHP-FPM status via FastCGI
$status = self::get_php_fpm_status();
if ( $status ) {
$info = array_merge( $info, $status );
}
}
return $info;
}
/**
* Parse PHP-FPM configuration file.
*
* @param string $config_file Path to config file.
* @return array|null Parsed configuration or null.
*/
private static function parse_php_fpm_config( $config_file ) {
$config = array();
$content = file_get_contents( $config_file );
if ( ! $content ) {
return null;
}
// Parse key configuration values (ignore comments after values)
$patterns = array(
'pool_name' => '/^\[([^\]]+)\]/m',
'pm_type' => '/^pm\s*=\s*(\w+)/m',
'max_children' => '/^pm\.max_children\s*=\s*(\d+)/m',
'start_servers' => '/^pm\.start_servers\s*=\s*(\d+)/m',
'min_spare_servers' => '/^pm\.min_spare_servers\s*=\s*(\d+)/m',
'max_spare_servers' => '/^pm\.max_spare_servers\s*=\s*(\d+)/m',
'max_requests' => '/^pm\.max_requests\s*=\s*(\d+)/m',
);
foreach ( $patterns as $key => $pattern ) {
if ( preg_match( $pattern, $content, $matches ) ) {
$config[ $key ] = trim( $matches[1] );
}
}
return $config;
}
/**
* Get PHP-FPM status from status endpoint.
*
* @return array|null Status information or null.
*/
private static function get_php_fpm_status() {
// Try common PHP-FPM status URLs
$status_urls = array(
'http://localhost/fpm-status',
'http://127.0.0.1/fpm-status',
'http://localhost/status',
);
foreach ( $status_urls as $url ) {
$response = wp_remote_get( $url . '?json' );
if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( $data ) {
return array(
'pool_name' => $data['pool'] ?? 'N/A',
'total_processes' => $data['total processes'] ?? 'N/A',
'active_processes' => $data['active processes'] ?? 'N/A',
'idle_processes' => $data['idle processes'] ?? 'N/A',
'listen_queue' => $data['listen queue'] ?? 'N/A',
'listen_queue_len' => $data['listen queue len'] ?? 'N/A',
'max_children' => $data['max children reached'] ?? 'N/A',
);
}
}
}
return null;
}
/**
* Get server information.
*
* @return array Server information.
*/
private static function get_server_info() {
return array(
'software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : 'Unknown',
'os' => PHP_OS,
'hostname' => gethostname(),
'server_addr' => isset( $_SERVER['SERVER_ADDR'] ) ? $_SERVER['SERVER_ADDR'] : 'Unknown',
'document_root' => isset( $_SERVER['DOCUMENT_ROOT'] ) ? $_SERVER['DOCUMENT_ROOT'] : 'Unknown',
'cpu_count' => self::get_cpu_count(),
'memory_total' => self::get_memory_total(),
'memory_available' => self::get_memory_available(),
'load_average' => self::get_load_average(),
);
}
/**
* Get CPU core count.
*
* @return int|string CPU count or 'Unknown'.
*/
private static function get_cpu_count() {
if ( function_exists( 'shell_exec' ) ) {
// Linux
$output = shell_exec( 'nproc 2>/dev/null' );
if ( $output ) {
return (int) trim( $output );
}
// macOS
$output = shell_exec( 'sysctl -n hw.ncpu 2>/dev/null' );
if ( $output ) {
return (int) trim( $output );
}
}
return 'Unknown';
}
/**
* Get total system memory in MB.
*
* @return string Memory in MB or 'Unknown'.
*/
private static function get_memory_total() {
if ( function_exists( 'shell_exec' ) ) {
// Linux
$output = shell_exec( "free -m | grep Mem | awk '{print $2}'" );
if ( $output ) {
return trim( $output ) . ' MB';
}
// macOS
$output = shell_exec( 'sysctl -n hw.memsize 2>/dev/null' );
if ( $output ) {
return round( (int) trim( $output ) / 1024 / 1024 ) . ' MB';
}
}
return 'Unknown';
}
/**
* Get available system memory in MB.
*
* @return string Available memory or 'Unknown'.
*/
private static function get_memory_available() {
if ( function_exists( 'shell_exec' ) ) {
// Linux
$output = shell_exec( "free -m | grep Mem | awk '{print $7}'" );
if ( $output ) {
return trim( $output ) . ' MB';
}
}
return 'Unknown';
}
/**
* Get system load average.
*
* @return string Load average or 'Unknown'.
*/
private static function get_load_average() {
if ( function_exists( 'sys_getloadavg' ) ) {
$load = sys_getloadavg();
return sprintf( '%.2f, %.2f, %.2f', $load[0], $load[1], $load[2] );
}
return 'Unknown';
}
/**
* Get WordPress information.
*
* @return array WordPress information.
*/
private static function get_wordpress_info() {
global $wp_version;
return array(
'version' => $wp_version,
'multisite' => is_multisite(),
'site_url' => get_site_url(),
'home_url' => get_home_url(),
'debug_mode' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'memory_limit' => WP_MEMORY_LIMIT,
'max_memory_limit' => WP_MAX_MEMORY_LIMIT,
'theme' => wp_get_theme()->get( 'Name' ),
'active_plugins' => count( get_option( 'active_plugins', array() ) ),
);
}
/**
* Get database information.
*
* @return array Database information.
*/
private static function get_database_info() {
global $wpdb;
$info = array(
'extension' => $wpdb->db_version(),
'server' => $wpdb->get_var( 'SELECT VERSION()' ),
'client_version' => $wpdb->db_version(),
'database_name' => DB_NAME,
'database_host' => DB_HOST,
'database_charset' => DB_CHARSET,
'table_prefix' => $wpdb->prefix,
);
// Get MySQL configuration
$variables = array(
'max_connections',
'max_allowed_packet',
'query_cache_size',
'innodb_buffer_pool_size',
'tmp_table_size',
'max_heap_table_size',
);
foreach ( $variables as $var ) {
$value = $wpdb->get_var( "SHOW VARIABLES LIKE '$var'" );
if ( $value ) {
$info[ $var ] = $wpdb->get_var( "SHOW VARIABLES LIKE '$var'", 1 );
}
}
// Get current connections
$info['current_connections'] = $wpdb->get_var( "SHOW STATUS LIKE 'Threads_connected'" )
? $wpdb->get_var( "SHOW STATUS LIKE 'Threads_connected'", 1 )
: 'Unknown';
return $info;
}
/**
* Get performance-related information.
*
* @return array Performance information.
*/
private static function get_performance_info() {
return array(
'object_cache' => wp_using_ext_object_cache() ? 'External' : 'Built-in',
'curl_available' => function_exists( 'curl_version' ),
'curl_version' => function_exists( 'curl_version' ) ? curl_version()['version'] : 'N/A',
'allow_url_fopen' => ini_get( 'allow_url_fopen' ) ? 'Yes' : 'No',
'compression' => extension_loaded( 'zlib' ) ? 'Available' : 'Not available',
'https_support' => is_ssl() ? 'Yes' : 'No',
);
}
/**
* Format system info as HTML.
*
* @param array $info System information array.
* @return string HTML output.
*/
public static function format_as_html( $info ) {
ob_start();
?>
<div class="maplepress-system-info">
<?php foreach ( $info as $section => $data ) : ?>
<div class="system-info-section">
<h3 style="margin-top: 0; padding: 10px; background: #f0f0f1; border-left: 4px solid #2271b1;">
<?php echo esc_html( ucwords( str_replace( '_', ' ', $section ) ) ); ?>
</h3>
<table class="widefat" style="margin-top: 0;">
<tbody>
<?php foreach ( $data as $key => $value ) : ?>
<?php
// Check if this is the opcache_status field (which contains a large array)
$is_opcache_status = ( $key === 'opcache_status' && is_array( $value ) );
$field_id = 'maplepress-field-' . sanitize_title( $section . '-' . $key );
?>
<tr>
<td style="width: 250px; font-weight: bold; vertical-align: top;">
<?php echo esc_html( ucwords( str_replace( '_', ' ', $key ) ) ); ?>
<?php if ( $is_opcache_status ) : ?>
<br>
<button type="button"
class="button button-secondary button-small maplepress-toggle-field"
data-target="<?php echo esc_attr( $field_id ); ?>"
style="margin-top: 5px;">
<span class="dashicons dashicons-visibility" style="vertical-align: middle; font-size: 14px;"></span>
<span class="toggle-text">Show</span>
</button>
<?php endif; ?>
</td>
<td>
<?php if ( $is_opcache_status ) : ?>
<div id="<?php echo esc_attr( $field_id ); ?>" style="display: none;">
<pre><?php echo esc_html( print_r( $value, true ) ); ?></pre>
</div>
<span class="opcache-hidden-notice" style="color: #666; font-style: italic;">
Click "Show" to view opcache details
</span>
<?php elseif ( is_array( $value ) ) : ?>
<pre><?php echo esc_html( print_r( $value, true ) ); ?></pre>
<?php elseif ( is_bool( $value ) ) : ?>
<?php echo $value ? '<span style="color: green;">✓ Yes</span>' : '<span style="color: red;">✗ No</span>'; ?>
<?php else : ?>
<?php echo esc_html( $value ); ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endforeach; ?>
</div>
<style>
.maplepress-system-info .system-info-section {
margin-bottom: 30px;
}
.maplepress-system-info pre {
margin: 0;
padding: 10px;
background: #f9f9f9;
border-left: 3px solid #ccc;
overflow-x: auto;
}
</style>
<script>
jQuery(document).ready(function($) {
// Toggle field visibility for opcache_status
$('.maplepress-toggle-field').on('click', function() {
var button = $(this);
var targetId = button.data('target');
var content = $('#' + targetId);
var notice = button.closest('tr').find('.opcache-hidden-notice');
var icon = button.find('.dashicons');
var text = button.find('.toggle-text');
if (content.is(':visible')) {
content.slideUp(300);
notice.show();
icon.removeClass('dashicons-hidden').addClass('dashicons-visibility');
text.text('Show');
} else {
content.slideDown(300);
notice.hide();
icon.removeClass('dashicons-visibility').addClass('dashicons-hidden');
text.text('Hide');
}
});
});
</script>
<?php
return ob_get_clean();
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* The core plugin class.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
class MaplePress {
/**
* The loader that's responsible for maintaining and registering all hooks.
*
* @var MaplePress_Loader
*/
protected $loader;
/**
* The unique identifier of this plugin.
*
* @var string
*/
protected $plugin_name;
/**
* The current version of the plugin.
*
* @var string
*/
protected $version;
/**
* Initialize the class and set its properties.
*/
public function __construct() {
$this->version = MAPLEPRESS_VERSION;
$this->plugin_name = 'maplepress';
$this->load_dependencies();
$this->define_admin_hooks();
$this->define_public_hooks();
}
/**
* Load the required dependencies for this plugin.
*/
private function load_dependencies() {
require_once MAPLEPRESS_PLUGIN_DIR . 'includes/class-maplepress-loader.php';
require_once MAPLEPRESS_PLUGIN_DIR . 'includes/class-maplepress-admin.php';
require_once MAPLEPRESS_PLUGIN_DIR . 'includes/class-maplepress-public.php';
require_once MAPLEPRESS_PLUGIN_DIR . 'includes/class-maplepress-api-client.php';
$this->loader = new MaplePress_Loader();
}
/**
* Register all hooks related to the admin area functionality.
*/
private function define_admin_hooks() {
$plugin_admin = new MaplePress_Admin( $this->get_plugin_name(), $this->get_version() );
$this->loader->add_action( 'admin_menu', $plugin_admin, 'add_plugin_admin_menu' );
$this->loader->add_action( 'admin_init', $plugin_admin, 'register_settings' );
$this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' );
$this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
}
/**
* Register all hooks related to the public-facing functionality.
*/
private function define_public_hooks() {
$plugin_public = new MaplePress_Public( $this->get_plugin_name(), $this->get_version() );
$this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
$this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
// Search integration
$this->loader->add_action( 'pre_get_posts', $plugin_public, 'intercept_search' );
$this->loader->add_filter( 'get_search_query', $plugin_public, 'restore_search_query' );
}
/**
* Run the loader to execute all hooks.
*/
public function run() {
$this->loader->run();
}
/**
* The name of the plugin.
*
* @return string
*/
public function get_plugin_name() {
return $this->plugin_name;
}
/**
* The reference to the class that orchestrates the hooks.
*
* @return MaplePress_Loader
*/
public function get_loader() {
return $this->loader;
}
/**
* Retrieve the version number of the plugin.
*
* @return string
*/
public function get_version() {
return $this->version;
}
}

View file

@ -0,0 +1,133 @@
<?php
/**
* Parallel Executor Class
*
* Executes search queries in parallel (concurrent requests) to simulate
* multiple simultaneous users searching the site.
*
* @package MaplePress_SearchSpeedTest
*/
class MPSS_Parallel_Executor {
/**
* Execute queries in parallel batches.
*
* @param array $queries Array of search query strings.
* @param int $concurrency Number of concurrent requests per batch.
* @return array Results array with timing and metadata.
*/
public function execute( $queries, $concurrency = 5 ) {
$results = array();
// Split queries into batches based on concurrency
$batches = array_chunk( $queries, $concurrency );
foreach ( $batches as $batch ) {
$batch_results = $this->execute_batch( $batch );
$results = array_merge( $results, $batch_results );
// Small delay between batches to prevent server overload
usleep( 50000 ); // 50ms
}
return $results;
}
/**
* Execute a batch of queries concurrently using cURL multi-handle.
*
* @param array $queries Array of query strings for this batch.
* @return array Results for this batch.
*/
private function execute_batch( $queries ) {
$results = array();
// Prepare requests
$requests = array();
foreach ( $queries as $query ) {
$search_url = add_query_arg( 's', urlencode( $query ), home_url( '/' ) );
$requests[] = array(
'url' => $search_url,
'query' => $query,
);
}
// Execute in parallel
$parallel_results = $this->execute_parallel_requests( $requests );
return $parallel_results;
}
/**
* Execute multiple HTTP requests in parallel using cURL multi-handle.
*
* @param array $requests Array of request data (url, query).
* @return array Results array.
*/
private function execute_parallel_requests( $requests ) {
$results = array();
$mh = curl_multi_init();
$curl_handles = array();
// Add all requests to multi-handle
foreach ( $requests as $index => $request ) {
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $request['url'] );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_TIMEOUT, 30 );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // For local testing
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // For local testing
// Add custom headers to identify test traffic (optional)
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'X-Speed-Test: MaplePress',
)
);
curl_multi_add_handle( $mh, $ch );
$curl_handles[ (int) $ch ] = array(
'handle' => $ch,
'query' => $request['query'],
'start' => microtime( true ),
);
}
// Execute all requests
$running = null;
do {
curl_multi_exec( $mh, $running );
curl_multi_select( $mh );
} while ( $running > 0 );
// Collect results
foreach ( $curl_handles as $data ) {
$info = curl_getinfo( $data['handle'] );
// Use cURL's total_time which accurately measures the individual request time
// NOT the time from batch start to collection (which would be incorrect)
$duration_ms = round( $info['total_time'] * 1000, 2 );
$results[] = array(
'query' => $data['query'],
'duration_ms' => $duration_ms,
'http_code' => $info['http_code'],
'timestamp' => $data['start'],
'success' => $info['http_code'] === 200,
);
curl_multi_remove_handle( $mh, $data['handle'] );
curl_close( $data['handle'] );
}
curl_multi_close( $mh );
return $results;
}
}

View file

@ -0,0 +1,278 @@
<?php
/**
* Query Generator Class
*
* Generates realistic search queries based on user behavior patterns.
*
* @package MaplePress_SearchSpeedTest
*/
class MPSS_Query_Generator {
/**
* Pre-defined realistic search query templates by category.
*
* @var array
*/
private $query_templates = array(
// Navigation queries (30%)
'navigation' => array(
'contact',
'about',
'home',
'services',
'pricing',
'support',
'login',
'register',
'cart',
'checkout',
'account',
'profile',
'settings',
'help',
'faq',
'terms',
'privacy',
'policy',
'sitemap',
'search',
),
// Informational queries (25%)
'informational' => array(
'how to',
'what is',
'why does',
'when should',
'where can',
'getting started',
'tutorial',
'guide',
'documentation',
'manual',
'learn',
'course',
'training',
'video',
'webinar',
'ebook',
'whitepaper',
'case study',
'best practices',
'tips',
),
// Product/Service queries (20%)
'product' => array(
'wordpress plugin',
'theme',
'template',
'widget',
'extension',
'free shipping',
'discount',
'coupon',
'promo',
'deal',
'best seller',
'popular',
'recommended',
'featured',
'top rated',
'new arrival',
'latest',
'coming soon',
'pre order',
'limited',
'on sale',
'clearance',
'bundle',
'package',
'subscription',
),
// Content discovery queries (15%)
'content' => array(
'blog',
'post',
'article',
'news',
'announcement',
'latest',
'recent',
'new',
'updated',
'archive',
'category',
'tag',
'topic',
'subject',
'theme',
'author',
'contributor',
'guest',
'interview',
'podcast',
),
// Troubleshooting queries (10%)
'troubleshooting' => array(
'error',
'not working',
'fix',
'repair',
'restore',
'problem',
'issue',
'bug',
'glitch',
'crash',
'troubleshoot',
'diagnose',
'solve',
'resolve',
'debug',
'broken',
'missing',
'invalid',
'failed',
'rejected',
),
);
/**
* Category weights for distribution.
*
* @var array
*/
private $category_weights = array(
'navigation' => 0.30,
'informational' => 0.25,
'product' => 0.20,
'content' => 0.15,
'troubleshooting' => 0.10,
);
/**
* Generate query set based on site size profile.
*
* @param string $profile_key Profile key (tiny, small, medium, big, gigantic).
* @param int $count Number of queries to generate.
* @return array Array of search query strings.
*/
public function generate_queries( $profile_key, $count ) {
$queries = array();
// Generate queries from each category based on weight
foreach ( $this->category_weights as $category => $weight ) {
$category_count = (int) ( $count * $weight );
$category_queries = $this->get_random_from_category( $category, $category_count );
$queries = array_merge( $queries, $category_queries );
}
// Shuffle to mix categories
shuffle( $queries );
// Add some variations (typos, mixed case, etc) - 20% of queries
$queries = $this->add_query_variations( $queries, 0.20 );
// Ensure we have exactly the requested count
return array_slice( $queries, 0, $count );
}
/**
* Get random queries from a specific category.
*
* @param string $category Category name.
* @param int $count Number of queries to get.
* @return array Array of queries.
*/
private function get_random_from_category( $category, $count ) {
if ( ! isset( $this->query_templates[ $category ] ) ) {
return array();
}
$available = $this->query_templates[ $category ];
// If we need more queries than available, allow duplicates
if ( $count > count( $available ) ) {
$queries = array();
for ( $i = 0; $i < $count; $i++ ) {
$queries[] = $available[ array_rand( $available ) ];
}
return $queries;
}
// Otherwise, shuffle and slice
shuffle( $available );
return array_slice( $available, 0, $count );
}
/**
* Add realistic query variations to the set.
*
* @param array $queries Base queries.
* @param float $variation_ratio Ratio of queries to vary (0.0-1.0).
* @return array Queries with variations added.
*/
private function add_query_variations( $queries, $variation_ratio = 0.20 ) {
$with_variations = array();
foreach ( $queries as $query ) {
// Always add original query
$with_variations[] = $query;
// Randomly add variations based on ratio
if ( mt_rand( 1, 100 ) <= ( $variation_ratio * 100 ) ) {
$variant = $this->create_variation( $query );
if ( $variant !== $query ) {
$with_variations[] = $variant;
}
}
}
return $with_variations;
}
/**
* Create a variation of a query (case changes, partial queries, suffixes).
*
* @param string $query Original query.
* @return string Variation of the query.
*/
private function create_variation( $query ) {
$rand = mt_rand( 1, 5 );
switch ( $rand ) {
case 1:
// All uppercase
return strtoupper( $query );
case 2:
// Title case
return ucwords( $query );
case 3:
// Partial query (first word only)
$words = explode( ' ', $query );
return $words[0];
case 4:
// Add common suffix
$suffixes = array( ' guide', ' help', ' info', ' page', ' wordpress' );
return $query . $suffixes[ array_rand( $suffixes ) ];
case 5:
// Mixed case (simulate typo)
$chars = str_split( $query );
$result = '';
foreach ( $chars as $char ) {
$result .= ( mt_rand( 0, 1 ) === 1 ) ? strtoupper( $char ) : strtolower( $char );
}
return $result;
}
return $query;
}
}

View file

@ -0,0 +1,224 @@
<?php
/**
* Results Analyzer Class
*
* Analyzes speed test results and generates recommendations.
*
* @package MaplePress_SearchSpeedTest
*/
class MPSS_Results_Analyzer {
/**
* Analyze speed test results.
*
* @param array $all_results All profile results.
* @return array Analyzed results with summary and recommendations.
*/
public function analyze( $all_results ) {
$summary = array();
foreach ( $all_results as $profile_key => $data ) {
$times = array_column( $data['results'], 'duration_ms' );
if ( empty( $times ) ) {
continue;
}
$avg_ms = array_sum( $times ) / count( $times );
$summary[ $profile_key ] = array(
'profile_name' => $data['profile']['name'],
'query_count' => count( $data['results'] ),
'avg_ms' => round( $avg_ms, 2 ),
'min_ms' => round( min( $times ), 2 ),
'max_ms' => round( max( $times ), 2 ),
'median_ms' => $this->median( $times ),
'p95_ms' => $this->percentile( $times, 95 ),
'p99_ms' => $this->percentile( $times, 99 ),
'status' => $this->get_status( $avg_ms ),
);
}
return array(
'summary' => $summary,
'recommendations' => $this->generate_recommendations( $summary ),
);
}
/**
* Calculate median of an array.
*
* @param array $array Numeric array.
* @return float Median value.
*/
private function median( $array ) {
sort( $array );
$count = count( $array );
$middle = floor( $count / 2 );
if ( $count % 2 === 0 ) {
return round( ( $array[ $middle - 1 ] + $array[ $middle ] ) / 2, 2 );
}
return round( $array[ $middle ], 2 );
}
/**
* Calculate percentile of an array.
*
* @param array $array Numeric array.
* @param int $percentile Percentile to calculate (e.g., 95 for P95).
* @return float Percentile value.
*/
private function percentile( $array, $percentile ) {
sort( $array );
$index = ceil( ( $percentile / 100 ) * count( $array ) ) - 1;
$index = max( 0, $index );
return round( $array[ $index ], 2 );
}
/**
* Get performance status based on average response time.
*
* @param float $avg_ms Average response time in milliseconds.
* @return string Status (excellent, good, fair, poor, critical).
*/
private function get_status( $avg_ms ) {
if ( $avg_ms < 50 ) {
return 'excellent';
} elseif ( $avg_ms < 150 ) {
return 'good';
} elseif ( $avg_ms < 500 ) {
return 'fair';
} elseif ( $avg_ms < 1000 ) {
return 'poor';
} else {
return 'critical';
}
}
/**
* Get current site information.
*
* @return array Site information.
*/
private function get_site_info() {
$post_count = wp_count_posts( 'post' );
$page_count = wp_count_posts( 'page' );
$published_posts = isset( $post_count->publish ) ? $post_count->publish : 0;
$published_pages = isset( $page_count->publish ) ? $page_count->publish : 0;
$total_content = $published_posts + $published_pages;
// Determine profile
if ( $total_content < 50 ) {
$profile = 'tiny';
} elseif ( $total_content < 500 ) {
$profile = 'small';
} elseif ( $total_content < 5000 ) {
$profile = 'medium';
} elseif ( $total_content < 50000 ) {
$profile = 'big';
} else {
$profile = 'gigantic';
}
return array(
'posts' => $published_posts,
'pages' => $published_pages,
'total_content' => $total_content,
'profile' => $profile,
);
}
/**
* Generate actionable recommendations based on results.
*
* @param array $summary Profile summaries.
* @return array Recommendations array.
*/
private function generate_recommendations( $summary ) {
$recommendations = array();
// Get the actual test results (could be 'custom' or any profile key)
$test_result = reset( $summary );
if ( ! $test_result ) {
return $recommendations;
}
$avg_ms = $test_result['avg_ms'];
// Performance-based recommendations
if ( $avg_ms < 50 ) {
$recommendations[] = array(
'level' => 'success',
'title' => __( 'Excellent Performance', 'maplepress-searchspeedtest' ),
'message' => sprintf(
/* translators: %s: average response time */
__( 'Average response time of %.2f ms is excellent! Your search is performing very well.', 'maplepress-searchspeedtest' ),
$avg_ms
),
);
} elseif ( $avg_ms < 150 ) {
$recommendations[] = array(
'level' => 'success',
'title' => __( 'Good Performance', 'maplepress-searchspeedtest' ),
'message' => sprintf(
/* translators: %s: average response time */
__( 'Average response time of %.2f ms is good. Your search is performing well.', 'maplepress-searchspeedtest' ),
$avg_ms
),
);
} elseif ( $avg_ms < 500 ) {
$recommendations[] = array(
'level' => 'warning',
'title' => __( 'Fair Performance - Room for Improvement', 'maplepress-searchspeedtest' ),
'message' => sprintf(
/* translators: %s: average response time */
__( 'Average response time of %.2f ms is acceptable but could be improved. Consider implementing search caching or optimizing your database queries.', 'maplepress-searchspeedtest' ),
$avg_ms
),
);
} elseif ( $avg_ms < 1000 ) {
$recommendations[] = array(
'level' => 'error',
'title' => __( 'Poor Performance', 'maplepress-searchspeedtest' ),
'message' => sprintf(
/* translators: %s: average response time */
__( 'Average response time of %.2f ms indicates performance issues. Consider database optimization, adding indexes, or using a dedicated search solution.', 'maplepress-searchspeedtest' ),
$avg_ms
),
);
} else {
$recommendations[] = array(
'level' => 'critical',
'title' => __( 'Critical: Very Slow Performance', 'maplepress-searchspeedtest' ),
'message' => sprintf(
/* translators: %s: average response time */
__( 'Average response time of %.2f ms is critically slow. Your database may need optimization, or you should consider a cloud-based search solution like MaplePress.', 'maplepress-searchspeedtest' ),
$avg_ms
),
);
}
// Add database optimization tip if showing slowness
if ( $avg_ms > 200 ) {
$recommendations[] = array(
'level' => 'info',
'title' => __( 'Database Optimization Tip', 'maplepress-searchspeedtest' ),
'message' => __( 'Consider adding database indexes on post_title and post_content columns, implementing search result caching, or using a cloud-based search solution like MaplePress for better performance.', 'maplepress-searchspeedtest' ),
);
}
// Add general optimization tips
$recommendations[] = array(
'level' => 'info',
'title' => __( 'Optimization Tips', 'maplepress-searchspeedtest' ),
'message' => __( 'Consider: 1) Implementing search result caching, 2) Adding database indexes on post_title and post_content, 3) Using a CDN, 4) Upgrading server resources, 5) Trying MaplePress for cloud-powered search.', 'maplepress-searchspeedtest' ),
);
return $recommendations;
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* Serial Executor Class
*
* Executes search queries sequentially (one after another) via HTTP requests.
* This ensures all WordPress hooks (including MaplePress search intercept) are triggered.
*
* @package MaplePress_SearchSpeedTest
*/
class MPSS_Serial_Executor {
/**
* Execute queries serially (one at a time) using WP_Query directly.
*
* @param array $queries Array of search query strings.
* @return array Results array with timing and metadata.
*/
public function execute( $queries ) {
$results = array();
foreach ( $queries as $index => $query ) {
// Start timing
$start_time = microtime( true );
// Execute WordPress search directly using WP_Query
$search_query = new WP_Query(
array(
's' => $query,
'post_type' => 'any',
'post_status' => 'publish',
'posts_per_page' => 10,
)
);
// End timing
$end_time = microtime( true );
$duration_ms = ( $end_time - $start_time ) * 1000;
$results[] = array(
'query' => $query,
'duration_ms' => round( $duration_ms, 2 ),
'result_count' => $search_query->found_posts,
'timestamp' => $start_time,
'success' => true,
);
// Reset post data
wp_reset_postdata();
// Small delay to prevent overwhelming the database (10ms)
usleep( 10000 );
}
return $results;
}
}

View file

@ -0,0 +1,340 @@
<?php
/**
* Simple Speed Test Class
*
* Just runs queries and times them. No complexity.
*
* @package MaplePress_SearchSpeedTest
*/
class MPSS_SpeedTest_Simple {
/**
* Run speed test with specified number of queries.
*
* @param int $query_count Number of queries to run.
* @param string $execution_mode Execution mode: 'serial' or 'parallel'.
* @return array Results with timing data.
*/
public function run( $query_count = 10, $execution_mode = 'serial' ) {
$start_time = microtime( true );
// Debug logging
error_log( 'MPSS_SpeedTest_Simple::run() called with mode: ' . $execution_mode );
// Generate search queries
$queries = $this->generate_queries( $query_count );
// Execute based on mode
if ( $execution_mode === 'parallel' ) {
error_log( 'MPSS: Executing in PARALLEL mode' );
$results = $this->run_parallel( $queries );
} else {
error_log( 'MPSS: Executing in SERIAL mode' );
$results = $this->run_serial( $queries );
}
$end_time = microtime( true );
$total_time = $end_time - $start_time;
// Calculate statistics
$times = array_column( $results, 'duration_ms' );
$avg_ms = array_sum( $times ) / count( $times );
$min_ms = min( $times );
$max_ms = max( $times );
return array(
'results' => $results,
'total_time' => round( $total_time, 2 ),
'total_queries' => count( $results ),
'avg_ms' => round( $avg_ms, 2 ),
'min_ms' => round( $min_ms, 2 ),
'max_ms' => round( $max_ms, 2 ),
'mode' => $execution_mode,
);
}
/**
* Run queries in serial mode (one at a time).
*
* @param array $queries Array of search query strings.
* @return array Results array.
*/
private function run_serial( $queries ) {
$results = array();
foreach ( $queries as $query ) {
$query_start = microtime( true );
// Make HTTP request to search URL to properly trigger MaplePress hooks
$search_url = home_url( '/?s=' . urlencode( $query ) );
$response = wp_remote_get(
$search_url,
array(
'timeout' => 30,
'headers' => array(
'X-MaplePress-Test' => '1',
),
)
);
$query_end = microtime( true );
$duration_ms = ( $query_end - $query_start ) * 1000;
// Extract result count from response - try multiple patterns
$result_count = 0;
if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
$body = wp_remote_retrieve_body( $response );
// Try various patterns to find result count
$patterns = array(
'/(\d+)\s+results?/i', // "5 results" or "1 result"
'/found\s+(\d+)/i', // "Found 5"
'/showing\s+(\d+)/i', // "Showing 5"
'/<article/i', // Count article tags
'/<div[^>]*class="[^"]*post[^"]*"/i', // Count post divs
);
foreach ( $patterns as $pattern ) {
if ( $pattern === '/<article/i' || strpos( $pattern, '<div' ) === 0 ) {
// Count HTML elements
$count = preg_match_all( $pattern, $body );
if ( $count > 0 ) {
$result_count = $count;
break;
}
} else {
// Extract number from text
if ( preg_match( $pattern, $body, $matches ) ) {
$result_count = intval( $matches[1] );
break;
}
}
}
}
$results[] = array(
'query' => $query,
'duration_ms' => round( $duration_ms, 2 ),
'result_count' => $result_count,
);
}
return $results;
}
/**
* Run queries in parallel mode (multiple concurrent requests).
*
* @param array $queries Array of search query strings.
* @return array Results array.
*/
private function run_parallel( $queries ) {
error_log( 'MPSS: run_parallel called with ' . count( $queries ) . ' queries' );
$results = array();
// Build search URLs
$search_urls = array();
foreach ( $queries as $query ) {
$search_urls[] = array(
'url' => home_url( '/?s=' . urlencode( $query ) ),
'query' => $query,
);
}
error_log( 'MPSS: Built ' . count( $search_urls ) . ' search URLs' );
// Initialize cURL multi handle
$mh = curl_multi_init();
$curl_handles = array();
$request_info = array();
// Add all requests to multi handle
foreach ( $search_urls as $index => $search_data ) {
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $search_data['url'] );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_TIMEOUT, 30 );
curl_setopt( $ch, CURLOPT_HTTPHEADER, array(
'X-MaplePress-Test: 1',
) );
curl_multi_add_handle( $mh, $ch );
$ch_id = (int) $ch;
$curl_handles[ $ch_id ] = $ch;
$request_info[ $ch_id ] = array(
'query' => $search_data['query'],
'index' => $index,
'start_time' => null,
'end_time' => null,
);
}
error_log( 'MPSS: Added ' . count( $curl_handles ) . ' handles, request_info has ' . count( $request_info ) . ' entries' );
// Execute all queries simultaneously and track start/end times
$running = null;
// Record start time for ALL requests (they all start together)
$batch_start = microtime( true );
foreach ( $request_info as $ch_id => $info ) {
$request_info[ $ch_id ]['start_time'] = $batch_start;
}
// Start executing - all requests run concurrently
do {
curl_multi_exec( $mh, $running );
// Check for completed requests and record their end time
while ( $done = curl_multi_info_read( $mh ) ) {
if ( $done['msg'] == CURLMSG_DONE ) {
$ch_id = (int) $done['handle'];
// Record when THIS specific request finished
if ( ! isset( $request_info[ $ch_id ]['end_time'] ) ) {
$request_info[ $ch_id ]['end_time'] = microtime( true );
}
}
}
if ( $running > 0 ) {
curl_multi_select( $mh );
}
} while ( $running > 0 );
$batch_end = microtime( true );
$batch_duration_ms = ( $batch_end - $batch_start ) * 1000;
error_log( 'MPSS: Batch completed in ' . round( $batch_duration_ms, 2 ) . 'ms, processing ' . count( $request_info ) . ' results' );
// Collect results - calculate duration from start/end times
foreach ( $request_info as $ch_id => $info ) {
$ch = $curl_handles[ $ch_id ];
// Calculate duration from start to end time
if ( isset( $info['start_time'] ) && isset( $info['end_time'] ) ) {
$duration_seconds = $info['end_time'] - $info['start_time'];
$duration_ms = $duration_seconds * 1000;
// Debug: log a few samples
static $debug_count = 0;
if ( $debug_count < 3 ) {
error_log( sprintf(
'MPSS: Sample #%d - start=%.6f, end=%.6f, duration_sec=%.6f, duration_ms=%.2f',
$debug_count,
$info['start_time'],
$info['end_time'],
$duration_seconds,
$duration_ms
) );
$debug_count++;
}
} else {
// Fallback - shouldn't happen
$duration_ms = 0;
}
$curl_info = curl_getinfo( $ch );
$response = curl_multi_getcontent( $ch );
$http_code = $curl_info['http_code'];
// Extract result count using multiple patterns
$result_count = 0;
if ( $http_code === 200 && ! empty( $response ) ) {
$patterns = array(
'/(\d+)\s+results?/i',
'/found\s+(\d+)/i',
'/showing\s+(\d+)/i',
'/<article/i',
'/<div[^>]*class="[^"]*post[^"]*"/i',
);
foreach ( $patterns as $pattern ) {
if ( $pattern === '/<article/i' || strpos( $pattern, '<div' ) === 0 ) {
$count = preg_match_all( $pattern, $response );
if ( $count > 0 ) {
$result_count = $count;
break;
}
} else {
if ( preg_match( $pattern, $response, $matches ) ) {
$result_count = intval( $matches[1] );
break;
}
}
}
}
$results[ $info['index'] ] = array(
'query' => $info['query'],
'duration_ms' => round( $duration_ms, 2 ),
'result_count' => $result_count,
);
curl_multi_remove_handle( $mh, $ch );
curl_close( $ch );
}
curl_multi_close( $mh );
// Sort results by original index to maintain query order
ksort( $results );
return array_values( $results );
}
/**
* Generate random search queries from existing content.
*
* @param int $count Number of queries to generate.
* @return array Array of search query strings.
*/
private function generate_queries( $count ) {
error_log( 'MPSS: generate_queries called with count=' . $count );
$posts = get_posts(
array(
'post_type' => array( 'post', 'page' ),
'post_status' => 'publish',
'posts_per_page' => $count * 3,
'orderby' => 'rand',
)
);
error_log( 'MPSS: get_posts returned ' . count( $posts ) . ' posts' );
$queries = array();
foreach ( $posts as $post ) {
if ( count( $queries ) >= $count ) {
break;
}
// Get words from title
$words = array_filter( explode( ' ', $post->post_title ) );
if ( empty( $words ) ) {
continue;
}
// 50% single word, 50% two words
if ( mt_rand( 1, 2 ) === 1 || count( $words ) === 1 ) {
$queries[] = $words[ array_rand( $words ) ];
} else {
$start = mt_rand( 0, count( $words ) - 2 );
$queries[] = $words[ $start ] . ' ' . $words[ $start + 1 ];
}
}
// If we don't have enough, pad with generic searches
while ( count( $queries ) < $count ) {
$queries[] = 'test';
}
$final_queries = array_slice( $queries, 0, $count );
error_log( 'MPSS: generate_queries returning ' . count( $final_queries ) . ' queries' );
return $final_queries;
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* Plugin Name: MaplePress
* Plugin URI: https://codeberg.org/mapleopentech/monorepo/src/branch/main/native/wordpress/maplepress-plugin
* Description: Fast, scalable search for WordPress - Offloads search processing to the cloud for lightning-fast results without slowing down your server
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Maple Open Technologies
* Author URI: https://mapleopentech.io
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: maplepress
* Domain Path: /languages
*
* @package MaplePress
*/
// If this file is called directly, abort.
if (!defined("WPINC")) {
die();
}
/**
* Current plugin version.
*/
define("MAPLEPRESS_VERSION", "1.0.0");
/**
* Plugin file path.
*/
define("MAPLEPRESS_PLUGIN_FILE", __FILE__);
/**
* Plugin directory path.
*/
define("MAPLEPRESS_PLUGIN_DIR", plugin_dir_path(__FILE__));
/**
* Plugin directory URL.
* Use set_url_scheme() to ensure correct protocol (HTTP/HTTPS) based on current request.
* This prevents mixed content issues when site is served over HTTPS.
*/
define("MAPLEPRESS_PLUGIN_URL", set_url_scheme(plugin_dir_url(__FILE__)));
/**
* The code that runs during plugin activation.
*/
function activate_maplepress()
{
require_once MAPLEPRESS_PLUGIN_DIR .
"includes/class-maplepress-activator.php";
MaplePress_Activator::activate();
}
/**
* The code that runs during plugin deactivation.
*/
function deactivate_maplepress()
{
require_once MAPLEPRESS_PLUGIN_DIR .
"includes/class-maplepress-deactivator.php";
MaplePress_Deactivator::deactivate();
}
register_activation_hook(__FILE__, "activate_maplepress");
register_deactivation_hook(__FILE__, "deactivate_maplepress");
/**
* The core plugin class.
*/
require MAPLEPRESS_PLUGIN_DIR . "includes/class-maplepress.php";
/**
* Begins execution of the plugin.
*/
function run_maplepress()
{
$plugin = new MaplePress();
$plugin->run();
}
run_maplepress();

View file

@ -0,0 +1,92 @@
=== MaplePress ===
Contributors: mapleopentech
Tags: search, cloud services, performance, scalable, cloud search, offload processing
Requires at least: 6.0
Tested up to: 6.4
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Cloud services platform for WordPress - Offloads computationally intensive tasks to cloud infrastructure. Currently features advanced search, with more services coming soon.
== Description ==
MaplePress connects your WordPress site to a cloud services platform that handles computationally intensive tasks remotely. Instead of burdening your WordPress server, MaplePress offloads processing to dedicated cloud infrastructure for better performance and scalability.
**Current Features:**
* **Cloud-powered search** - Lightning-fast search with advanced indexing
* **Scalable infrastructure** - Handles high-traffic effortlessly
* **Offloaded processing** - Frees up your WordPress server resources
* **Automatic content indexing** - Content synced to the cloud automatically
* **Centralized management** - Manage multiple sites from one dashboard
* **Modern technology** - Better performance than default WordPress search
**Coming Soon:**
* Cloud-powered file uploads and storage
* Advanced metrics and analytics
* Performance monitoring
* And more cloud services to enhance your WordPress site
**How It Works:**
1. Sign up for a MaplePress account
2. Install and activate this plugin
3. Enter your API key in the settings
4. MaplePress begins handling cloud services for your site
5. Your visitors enjoy better performance and enhanced features
**Requirements:**
* WordPress 6.0 or higher
* PHP 7.4 or higher
* MaplePress account (sign up at https://mapleopentech.io)
== Installation ==
1. Upload the plugin files to `/wp-content/plugins/maplepress-plugin/`, or install through the WordPress plugins screen
2. Activate the plugin through the 'Plugins' screen in WordPress
3. Go to Settings > MaplePress to configure the plugin
4. Enter your MaplePress API URL and API Key
5. Enable the plugin and save settings
== Frequently Asked Questions ==
= Do I need a MaplePress account? =
Yes, you need a MaplePress account to use this plugin. Sign up at https://mapleopentech.io
= Is my data secure? =
Yes, all communication between your WordPress site and MaplePress backend is encrypted. Your API key is stored securely.
= How often is content indexed? =
Content is indexed automatically whenever you create, update, or delete posts and pages.
= Can I customize the search results? =
Yes, the plugin provides filters and hooks for developers to customize search behavior and results display.
== Screenshots ==
1. MaplePress settings page with connection status
2. Cloud services dashboard showing usage metrics
3. Search results powered by cloud infrastructure
== Changelog ==
= 1.0.0 =
* Initial release
* Cloud services platform integration
* Cloud-powered search with automatic indexing
* Settings page for API configuration
* Connection verification and usage monitoring
* Foundation for future cloud services (uploads, metrics, etc.)
== Upgrade Notice ==
= 1.0.0 =
Initial release of MaplePress plugin.

View file

@ -0,0 +1,23 @@
<?php
/**
* Fired when the plugin is uninstalled.
*
* @package MaplePress
* @subpackage MaplePress/includes
*/
// If uninstall not called from WordPress, then exit.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete all plugin options
delete_option( 'maplepress_settings' );
// Delete any transients
delete_transient( 'maplepress_activation_redirect' );
delete_transient( 'maplepress_redirect_to_sync' );
delete_transient( 'maplepress_pending_deletion' );
// Log the uninstall
error_log( 'MaplePress: Plugin uninstalled and all data removed' );