Implement Phase 9: Prometheus Metrics (v0.9.0)

- Add Prometheus metrics integration via wp-prometheus hooks
- Create Grafana dashboard JSON with 24 panels
- Add metrics settings tab with enable/disable toggle
- Expose inventory, booking, occupancy, revenue, and guest metrics

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 20:40:54 +01:00
parent b6d7eeb5ec
commit b7bdfe256f
8 changed files with 2805 additions and 8 deletions

View File

@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.9.0] - 2026-02-03
### Added
- Prometheus Metrics Integration:
- New `src/Integration/Prometheus.php` class for metrics collection
- Integration with wp-prometheus plugin via `wp_prometheus_collect_metrics` hook
- Inventory metrics: buildings total, rooms by status, services by status
- Booking metrics: bookings by status, check-ins/check-outs today, upcoming 7 days, avg duration
- Guest metrics: total guests, guests by status, repeat guests, new guests this month
- Occupancy metrics: current rate, monthly rate, occupied rooms, total bed capacity
- Revenue metrics: this month, YTD, average booking value, services revenue
- Grafana Dashboard:
- Pre-configured dashboard at `assets/grafana/wp-bnb-dashboard.json`
- Automatic registration with wp-prometheus dashboard provider
- Occupancy gauges with color-coded thresholds
- Pie charts for bookings, rooms, and guests by status
- Revenue and guest statistics panels
- Responsive grid layout with 24 panels
- Settings page Metrics tab:
- Enable/disable metrics collection toggle
- WP Prometheus detection with status indicator
- Complete metrics reference table
- Dashboard file location and export info
### Changed
- Plugin.php updated to initialize Prometheus integration
- Settings page now has six tabs: General, Pricing, License, Updates, Metrics
## [0.8.0] - 2026-02-03 ## [0.8.0] - 2026-02-03
### Added ### Added

View File

@@ -935,3 +935,60 @@ Admin features always work; frontend requires valid license.
- CSV export with BOM (`\xEF\xBB\xBF`) ensures Excel compatibility - CSV export with BOM (`\xEF\xBB\xBF`) ensures Excel compatibility
- Guest data aggregation from bookings uses unique key pattern for anonymous guests - Guest data aggregation from bookings uses unique key pattern for anonymous guests
- Occupancy calculation: (booked nights / total room nights) * 100 - Occupancy calculation: (booked nights / total room nights) * 100
### 2026-02-03 - Version 0.9.0 (Prometheus Metrics)
**Completed:**
- Created `src/Integration/Prometheus.php` class (~700 lines)
- Integration with wp-prometheus via `wp_prometheus_collect_metrics` hook
- Dashboard registration via `wp_prometheus_register_dashboards` hook
- Option to enable/disable metrics collection
- Inventory metrics: buildings total, rooms by status, services by status
- Booking metrics: by status, check-ins/outs today, upcoming 7 days, avg duration
- Guest metrics: total, by status, repeat guests, new this month
- Occupancy metrics: current rate, monthly rate, occupied rooms, bed capacity
- Revenue metrics: this month, YTD, avg booking value, services revenue
- Optimized SQL queries using `$wpdb->prepare()` throughout
- Created `assets/grafana/wp-bnb-dashboard.json` Grafana dashboard
- 24 panels with responsive grid layout
- Occupancy gauges with color-coded thresholds (red < 30%, orange < 50%, yellow < 70%, green ≥ 70%)
- Pie charts for bookings, rooms, and guests by status
- Revenue stat panels (this month, YTD, avg value, services)
- Guest stat panels (total, new, repeat, active services)
- Today's activity panels (check-ins, check-outs, upcoming)
- Prometheus datasource variable for flexibility
- Auto-refresh every 5 minutes
- Updated `src/Plugin.php`
- Added Prometheus class import
- Initialized Prometheus integration in `init_components()`
- Added "Metrics" tab to settings page (6 tabs total)
- Added `render_metrics_settings()` method with WP Prometheus detection
- Added `save_metrics_settings()` method
- Metrics reference table showing all available metrics
- Updated version to 0.9.0
**Files Created:**
- `src/Integration/Prometheus.php` - Prometheus metrics integration class
- `assets/grafana/wp-bnb-dashboard.json` - Pre-configured Grafana dashboard
**Files Changed:**
- `src/Plugin.php` - Prometheus initialization, metrics settings tab
- `wp-bnb.php` - Version bump to 0.9.0 (header and constant)
- `CHANGELOG.md` - Added v0.9.0 release notes
- `PLAN.md` - Marked Phase 9 as complete
- `README.md` - Added Prometheus metrics documentation
**Learnings:**
- wp-prometheus uses `wp_prometheus_collect_metrics` action with collector object
- Collector provides `register_gauge()` for fluctuating values
- Labels are passed as array to `register_gauge()`, values to `set()`
- Grafana dashboard JSON requires proper panel IDs and grid positions
- Occupancy queries need careful date range handling for month boundaries
- Revenue queries use `DECIMAL(10,2)` casting for accurate sums
- Metrics should be cached or computed efficiently as they're scraped frequently
- Dashboard registration requires file path, title, description, icon, and plugin name
- Settings tab detection uses `$prometheus_active` to show WP Prometheus status

20
PLAN.md
View File

@@ -180,15 +180,23 @@ This document outlines the implementation plan for the WP BnB Management plugin.
- [x] Guest statistics - [x] Guest statistics
- [x] Export functionality (CSV, PDF) - [x] Export functionality (CSV, PDF)
## Phase 9: Prometheus Metrics (v0.9.0) ## Phase 9: Prometheus Metrics (v0.9.0) - Complete
- [ ] Meanigful Metrics for this Plugin, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details - [x] Meaningful Metrics for this Plugin:
- [ ] Example Grafana-Dashboard, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details - Inventory: buildings, rooms by status, services by status
- [ ] Update settings page to enable/disable metrics - Bookings: by status, check-ins/check-outs today, upcoming, avg duration
- Guests: total, by status, repeat guests, new this month
- Occupancy: current rate, monthly rate, occupied rooms, bed capacity
- Revenue: this month, YTD, average booking value, services revenue
- [x] Example Grafana Dashboard:
- Pre-configured dashboard JSON at `assets/grafana/wp-bnb-dashboard.json`
- Automatic registration with wp-prometheus
- 24 panels with gauges, pie charts, and stat displays
- [x] Update settings page to enable/disable metrics
## Phase 10: Security Audit (v0.10.0) ## Phase 10: Security Audit (v0.10.0)
- [ ] Check for Wordpress best-practises - [ ] Check for Wordpress best-practices
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads - [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
## Future Considerations (v1.0.0+) ## Future Considerations (v1.0.0+)
@@ -308,6 +316,6 @@ The plugin will provide extensive hooks for customization:
| 0.6.0 | Frontend | Complete | | 0.6.0 | Frontend | Complete |
| 0.7.0 | CF7 Integration | Complete | | 0.7.0 | CF7 Integration | Complete |
| 0.8.0 | Dashboard | Complete | | 0.8.0 | Dashboard | Complete |
| 0.9.0 | Prometheus Metrics | TBD | | 0.9.0 | Prometheus Metrics | Complete |
| 0.10.0 | Security Audit | TBD | | 0.10.0 | Security Audit | TBD |
| 1.0.0 | Stable Release | TBD | | 1.0.0 | Stable Release | TBD |

View File

@@ -21,6 +21,7 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms - **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
- **Dashboard**: Comprehensive admin dashboard with statistics and charts - **Dashboard**: Comprehensive admin dashboard with statistics and charts
- **Reports**: Detailed reports with CSV and PDF export - **Reports**: Detailed reports with CSV and PDF export
- **Prometheus Metrics**: Expose operational metrics for monitoring with Grafana
### Requirements ### Requirements
@@ -383,6 +384,66 @@ add_action( 'wp_bnb_before_booking_create', function( $booking_data ) {
} ); } );
``` ```
## Prometheus Metrics
The plugin integrates with [WP Prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) to expose operational metrics for monitoring with Prometheus and Grafana.
### Enabling Metrics
1. Install and activate the WP Prometheus plugin
2. Navigate to **WP BnB → Settings → Metrics**
3. Enable "Expose BnB metrics via Prometheus"
4. Metrics will be available at your site's `/metrics/` endpoint
### Available Metrics
**Inventory Metrics:**
- `wp_bnb_buildings_total` - Total number of buildings
- `wp_bnb_rooms_total{status}` - Rooms by status (available, occupied, maintenance, inactive)
- `wp_bnb_services_total{status}` - Services by status (active, inactive)
- `wp_bnb_total_capacity_beds` - Total bed capacity across all rooms
**Booking Metrics:**
- `wp_bnb_bookings_total{status}` - Bookings by status (pending, confirmed, checked_in, checked_out, cancelled)
- `wp_bnb_checkins_today` - Check-ins scheduled for today
- `wp_bnb_checkouts_today` - Check-outs scheduled for today
- `wp_bnb_bookings_upcoming_7days` - Bookings starting in next 7 days
- `wp_bnb_booking_avg_duration_nights` - Average booking duration
**Occupancy Metrics:**
- `wp_bnb_occupancy_rate_current` - Current room occupancy rate (percentage)
- `wp_bnb_occupancy_rate_this_month` - Monthly occupancy rate (percentage)
- `wp_bnb_rooms_currently_occupied` - Rooms currently occupied
**Revenue Metrics:**
- `wp_bnb_revenue_this_month{currency}` - Revenue for current month
- `wp_bnb_revenue_ytd{currency}` - Revenue year to date
- `wp_bnb_booking_avg_value{currency}` - Average booking value
- `wp_bnb_services_revenue_this_month{currency}` - Services revenue this month
**Guest Metrics:**
- `wp_bnb_guests_total` - Total registered guests
- `wp_bnb_guests_by_status{status}` - Guests by status (active, blocked, vip)
- `wp_bnb_guests_repeat` - Guests with more than one booking
- `wp_bnb_guests_new_this_month` - New guests this month
### Grafana Dashboard
A pre-configured Grafana dashboard is included at `assets/grafana/wp-bnb-dashboard.json`. If WP Prometheus is installed, the dashboard is automatically registered and available for export.
The dashboard includes:
- Occupancy gauges with color-coded thresholds
- Bookings, rooms, and guests pie charts by status
- Revenue and guest statistics panels
- Today's check-ins/check-outs
- Trend indicators
## Frequently Asked Questions ## Frequently Asked Questions
### Do I need a license to use this plugin? ### Do I need a license to use this plugin?

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,877 @@
<?php
/**
* Prometheus Metrics Integration.
*
* Provides meaningful metrics for monitoring BnB operations.
*
* @package Magdev\WpBnb\Integration
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Integration;
use Magdev\WpBnb\PostTypes\Booking;
use Magdev\WpBnb\PostTypes\Building;
use Magdev\WpBnb\PostTypes\Guest;
use Magdev\WpBnb\PostTypes\Room;
use Magdev\WpBnb\PostTypes\Service;
/**
* Prometheus Metrics Integration class.
*
* Exposes BnB metrics via the wp-prometheus plugin.
*/
class Prometheus {
/**
* Option key for enabling metrics.
*/
public const OPTION_ENABLED = 'wp_bnb_metrics_enabled';
/**
* Initialize the Prometheus integration.
*
* @return void
*/
public static function init(): void {
// Only hook if metrics are enabled.
if ( ! self::is_enabled() ) {
return;
}
// Hook into wp-prometheus collector.
add_action( 'wp_prometheus_collect_metrics', array( self::class, 'collect_metrics' ) );
// Register Grafana dashboard.
add_action( 'wp_prometheus_register_dashboards', array( self::class, 'register_dashboards' ) );
}
/**
* Check if metrics collection is enabled.
*
* @return bool
*/
public static function is_enabled(): bool {
return 'yes' === get_option( self::OPTION_ENABLED, 'yes' );
}
/**
* Enable metrics collection.
*
* @return void
*/
public static function enable(): void {
update_option( self::OPTION_ENABLED, 'yes' );
}
/**
* Disable metrics collection.
*
* @return void
*/
public static function disable(): void {
update_option( self::OPTION_ENABLED, 'no' );
}
/**
* Collect and register all BnB metrics.
*
* @param object $collector The wp-prometheus collector instance.
* @return void
*/
public static function collect_metrics( $collector ): void {
self::collect_inventory_metrics( $collector );
self::collect_booking_metrics( $collector );
self::collect_guest_metrics( $collector );
self::collect_occupancy_metrics( $collector );
self::collect_revenue_metrics( $collector );
}
/**
* Collect inventory metrics (buildings, rooms, services).
*
* @param object $collector The wp-prometheus collector instance.
* @return void
*/
private static function collect_inventory_metrics( $collector ): void {
// Buildings total.
$buildings_total = wp_count_posts( Building::POST_TYPE );
$gauge = $collector->register_gauge(
'wp_bnb_buildings_total',
'Total number of buildings',
array()
);
$gauge->set( (int) $buildings_total->publish, array() );
// Rooms by status.
$rooms_gauge = $collector->register_gauge(
'wp_bnb_rooms_total',
'Total number of rooms by status',
array( 'status' )
);
$room_statuses = array( 'available', 'occupied', 'maintenance', 'inactive' );
foreach ( $room_statuses as $status ) {
$count = self::count_rooms_by_status( $status );
$rooms_gauge->set( $count, array( $status ) );
}
// Services by status.
$services_gauge = $collector->register_gauge(
'wp_bnb_services_total',
'Total number of services by status',
array( 'status' )
);
$service_statuses = array( 'active', 'inactive' );
foreach ( $service_statuses as $status ) {
$count = self::count_services_by_status( $status );
$services_gauge->set( $count, array( $status ) );
}
}
/**
* Collect booking metrics.
*
* @param object $collector The wp-prometheus collector instance.
* @return void
*/
private static function collect_booking_metrics( $collector ): void {
// Bookings by status.
$bookings_gauge = $collector->register_gauge(
'wp_bnb_bookings_total',
'Total number of bookings by status',
array( 'status' )
);
$booking_statuses = array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' );
foreach ( $booking_statuses as $status ) {
$count = self::count_bookings_by_status( $status );
$bookings_gauge->set( $count, array( $status ) );
}
// Today's check-ins.
$checkins_gauge = $collector->register_gauge(
'wp_bnb_checkins_today',
'Number of check-ins scheduled for today',
array()
);
$checkins_gauge->set( self::count_todays_checkins(), array() );
// Today's check-outs.
$checkouts_gauge = $collector->register_gauge(
'wp_bnb_checkouts_today',
'Number of check-outs scheduled for today',
array()
);
$checkouts_gauge->set( self::count_todays_checkouts(), array() );
// Upcoming bookings (next 7 days).
$upcoming_gauge = $collector->register_gauge(
'wp_bnb_bookings_upcoming_7days',
'Number of bookings starting in the next 7 days',
array()
);
$upcoming_gauge->set( self::count_upcoming_bookings( 7 ), array() );
// Average booking duration (nights).
$avg_duration = $collector->register_gauge(
'wp_bnb_booking_avg_duration_nights',
'Average booking duration in nights',
array()
);
$avg_duration->set( self::get_average_booking_duration(), array() );
}
/**
* Collect guest metrics.
*
* @param object $collector The wp-prometheus collector instance.
* @return void
*/
private static function collect_guest_metrics( $collector ): void {
// Total guests.
$guests_total = wp_count_posts( Guest::POST_TYPE );
$guests_gauge = $collector->register_gauge(
'wp_bnb_guests_total',
'Total number of registered guests',
array()
);
$guests_gauge->set( (int) $guests_total->publish, array() );
// Guests by status.
$guests_status_gauge = $collector->register_gauge(
'wp_bnb_guests_by_status',
'Number of guests by status',
array( 'status' )
);
$guest_statuses = array( 'active', 'blocked', 'vip' );
foreach ( $guest_statuses as $status ) {
$count = self::count_guests_by_status( $status );
$guests_status_gauge->set( $count, array( $status ) );
}
// Repeat guests (guests with more than one booking).
$repeat_gauge = $collector->register_gauge(
'wp_bnb_guests_repeat',
'Number of guests with more than one booking',
array()
);
$repeat_gauge->set( self::count_repeat_guests(), array() );
// New guests this month.
$new_guests_gauge = $collector->register_gauge(
'wp_bnb_guests_new_this_month',
'Number of new guests registered this month',
array()
);
$new_guests_gauge->set( self::count_new_guests_this_month(), array() );
}
/**
* Collect occupancy metrics.
*
* @param object $collector The wp-prometheus collector instance.
* @return void
*/
private static function collect_occupancy_metrics( $collector ): void {
// Current occupancy rate (percentage).
$occupancy_gauge = $collector->register_gauge(
'wp_bnb_occupancy_rate_current',
'Current room occupancy rate (percentage)',
array()
);
$occupancy_gauge->set( self::get_current_occupancy_rate(), array() );
// Occupancy rate this month.
$occupancy_month_gauge = $collector->register_gauge(
'wp_bnb_occupancy_rate_this_month',
'Room occupancy rate for the current month (percentage)',
array()
);
$occupancy_month_gauge->set( self::get_monthly_occupancy_rate(), array() );
// Rooms currently occupied.
$occupied_gauge = $collector->register_gauge(
'wp_bnb_rooms_currently_occupied',
'Number of rooms currently occupied',
array()
);
$occupied_gauge->set( self::count_currently_occupied_rooms(), array() );
// Total room capacity (beds).
$capacity_gauge = $collector->register_gauge(
'wp_bnb_total_capacity_beds',
'Total bed capacity across all rooms',
array()
);
$capacity_gauge->set( self::get_total_bed_capacity(), array() );
}
/**
* Collect revenue metrics.
*
* @param object $collector The wp-prometheus collector instance.
* @return void
*/
private static function collect_revenue_metrics( $collector ): void {
$currency = get_option( 'wp_bnb_currency', 'CHF' );
// Revenue this month.
$revenue_month_gauge = $collector->register_gauge(
'wp_bnb_revenue_this_month',
'Total revenue for the current month',
array( 'currency' )
);
$revenue_month_gauge->set( self::get_revenue_this_month(), array( $currency ) );
// Revenue year to date.
$revenue_ytd_gauge = $collector->register_gauge(
'wp_bnb_revenue_ytd',
'Total revenue year to date',
array( 'currency' )
);
$revenue_ytd_gauge->set( self::get_revenue_ytd(), array( $currency ) );
// Average booking value.
$avg_value_gauge = $collector->register_gauge(
'wp_bnb_booking_avg_value',
'Average booking value',
array( 'currency' )
);
$avg_value_gauge->set( self::get_average_booking_value(), array( $currency ) );
// Revenue from services this month.
$services_revenue_gauge = $collector->register_gauge(
'wp_bnb_services_revenue_this_month',
'Revenue from additional services this month',
array( 'currency' )
);
$services_revenue_gauge->set( self::get_services_revenue_this_month(), array( $currency ) );
}
/**
* Register Grafana dashboards.
*
* @param object $provider The wp-prometheus dashboard provider instance.
* @return void
*/
public static function register_dashboards( $provider ): void {
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
if ( file_exists( $dashboard_file ) ) {
$provider->register_dashboard(
'wp-bnb',
array(
'title' => __( 'WP BnB Dashboard', 'wp-bnb' ),
'description' => __( 'Monitor occupancy, bookings, revenue, and guest statistics for your B&B.', 'wp-bnb' ),
'icon' => 'dashicons-building',
'file' => $dashboard_file,
'plugin' => 'WP BnB Manager',
)
);
}
}
// =========================================================================
// Helper Methods - Inventory
// =========================================================================
/**
* Count rooms by status.
*
* @param string $status Room status.
* @return int
*/
private static function count_rooms_by_status( string $status ): int {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm.meta_key = '_bnb_room_status'
AND pm.meta_value = %s",
Room::POST_TYPE,
$status
)
);
}
/**
* Count services by status.
*
* @param string $status Service status.
* @return int
*/
private static function count_services_by_status( string $status ): int {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm.meta_key = '_bnb_service_status'
AND pm.meta_value = %s",
Service::POST_TYPE,
$status
)
);
}
// =========================================================================
// Helper Methods - Bookings
// =========================================================================
/**
* Count bookings by status.
*
* @param string $status Booking status.
* @return int
*/
private static function count_bookings_by_status( string $status ): int {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm.meta_key = '_bnb_booking_status'
AND pm.meta_value = %s",
Booking::POST_TYPE,
$status
)
);
}
/**
* Count today's check-ins.
*
* @return int
*/
private static function count_todays_checkins(): int {
global $wpdb;
$today = gmdate( 'Y-m-d' );
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_date.meta_key = '_bnb_booking_check_in'
AND pm_date.meta_value = %s
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'pending')",
Booking::POST_TYPE,
$today
)
);
}
/**
* Count today's check-outs.
*
* @return int
*/
private static function count_todays_checkouts(): int {
global $wpdb;
$today = gmdate( 'Y-m-d' );
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_date.meta_key = '_bnb_booking_check_out'
AND pm_date.meta_value = %s
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value = 'checked_in'",
Booking::POST_TYPE,
$today
)
);
}
/**
* Count upcoming bookings within given days.
*
* @param int $days Number of days to look ahead.
* @return int
*/
private static function count_upcoming_bookings( int $days ): int {
global $wpdb;
$today = gmdate( 'Y-m-d' );
$end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) );
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_date.meta_key = '_bnb_booking_check_in'
AND pm_date.meta_value >= %s
AND pm_date.meta_value <= %s
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'pending')",
Booking::POST_TYPE,
$today,
$end_date
)
);
}
/**
* Get average booking duration in nights.
*
* @return float
*/
private static function get_average_booking_duration(): float {
global $wpdb;
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT AVG(DATEDIFF(pm_out.meta_value, pm_in.meta_value))
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_in.meta_key = '_bnb_booking_check_in'
AND pm_out.meta_key = '_bnb_booking_check_out'
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value NOT IN ('cancelled')",
Booking::POST_TYPE
)
);
return round( (float) $result, 1 );
}
// =========================================================================
// Helper Methods - Guests
// =========================================================================
/**
* Count guests by status.
*
* @param string $status Guest status.
* @return int
*/
private static function count_guests_by_status( string $status ): int {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm.meta_key = '_bnb_guest_status'
AND pm.meta_value = %s",
Guest::POST_TYPE,
$status
)
);
}
/**
* Count repeat guests (more than one booking).
*
* @return int
*/
private static function count_repeat_guests(): int {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT pm.meta_value)
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm.meta_key = '_bnb_booking_guest_id'
AND pm.meta_value != ''
GROUP BY pm.meta_value
HAVING COUNT(*) > 1",
Booking::POST_TYPE
)
);
}
/**
* Count new guests registered this month.
*
* @return int
*/
private static function count_new_guests_this_month(): int {
global $wpdb;
$first_of_month = gmdate( 'Y-m-01 00:00:00' );
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->posts}
WHERE post_type = %s
AND post_status = 'publish'
AND post_date >= %s",
Guest::POST_TYPE,
$first_of_month
)
);
}
// =========================================================================
// Helper Methods - Occupancy
// =========================================================================
/**
* Get current occupancy rate (percentage of rooms occupied today).
*
* @return float
*/
private static function get_current_occupancy_rate(): float {
$total_rooms = self::count_available_rooms();
if ( $total_rooms <= 0 ) {
return 0.0;
}
$occupied = self::count_currently_occupied_rooms();
return round( ( $occupied / $total_rooms ) * 100, 1 );
}
/**
* Get monthly occupancy rate.
*
* @return float
*/
private static function get_monthly_occupancy_rate(): float {
global $wpdb;
$total_rooms = self::count_available_rooms();
if ( $total_rooms <= 0 ) {
return 0.0;
}
$first_of_month = gmdate( 'Y-m-01' );
$today = gmdate( 'Y-m-d' );
$days_so_far = (int) gmdate( 'd' );
$total_room_nights = $total_rooms * $days_so_far;
// Count booked nights this month (simplified: count bookings that overlap with this month).
$booked_nights = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(
DATEDIFF(
LEAST(pm_out.meta_value, %s),
GREATEST(pm_in.meta_value, %s)
)
)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_in.meta_key = '_bnb_booking_check_in'
AND pm_out.meta_key = '_bnb_booking_check_out'
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')
AND pm_in.meta_value <= %s
AND pm_out.meta_value >= %s",
$today,
$first_of_month,
Booking::POST_TYPE,
$today,
$first_of_month
)
);
if ( $total_room_nights <= 0 ) {
return 0.0;
}
return round( ( $booked_nights / $total_room_nights ) * 100, 1 );
}
/**
* Count currently occupied rooms.
*
* @return int
*/
private static function count_currently_occupied_rooms(): int {
global $wpdb;
$today = gmdate( 'Y-m-d' );
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT pm_room.meta_value)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_room ON p.ID = pm_room.post_id
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_room.meta_key = '_bnb_booking_room_id'
AND pm_in.meta_key = '_bnb_booking_check_in'
AND pm_out.meta_key = '_bnb_booking_check_out'
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'checked_in')
AND pm_in.meta_value <= %s
AND pm_out.meta_value > %s",
Booking::POST_TYPE,
$today,
$today
)
);
}
/**
* Count available rooms (not in maintenance/inactive).
*
* @return int
*/
private static function count_available_rooms(): int {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_bnb_room_status'
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND (pm.meta_value IS NULL OR pm.meta_value IN ('available', 'occupied'))",
Room::POST_TYPE
)
);
}
/**
* Get total bed capacity.
*
* @return int
*/
private static function get_total_bed_capacity(): int {
global $wpdb;
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(CAST(pm.meta_value AS UNSIGNED))
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm.meta_key = '_bnb_room_beds'",
Room::POST_TYPE
)
);
return (int) $result;
}
// =========================================================================
// Helper Methods - Revenue
// =========================================================================
/**
* Get revenue for the current month.
*
* @return float
*/
private static function get_revenue_this_month(): float {
global $wpdb;
$first_of_month = gmdate( 'Y-m-01' );
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_price.meta_key = '_bnb_booking_total_price'
AND pm_in.meta_key = '_bnb_booking_check_in'
AND pm_in.meta_value >= %s
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
Booking::POST_TYPE,
$first_of_month
)
);
return round( (float) $result, 2 );
}
/**
* Get revenue year to date.
*
* @return float
*/
private static function get_revenue_ytd(): float {
global $wpdb;
$first_of_year = gmdate( 'Y-01-01' );
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_price.meta_key = '_bnb_booking_total_price'
AND pm_in.meta_key = '_bnb_booking_check_in'
AND pm_in.meta_value >= %s
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
Booking::POST_TYPE,
$first_of_year
)
);
return round( (float) $result, 2 );
}
/**
* Get average booking value.
*
* @return float
*/
private static function get_average_booking_value(): float {
global $wpdb;
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT AVG(CAST(pm_price.meta_value AS DECIMAL(10,2)))
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_price.meta_key = '_bnb_booking_total_price'
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
Booking::POST_TYPE
)
);
return round( (float) $result, 2 );
}
/**
* Get revenue from services this month.
*
* @return float
*/
private static function get_services_revenue_this_month(): float {
global $wpdb;
$first_of_month = gmdate( 'Y-m-01' );
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(CAST(pm_services.meta_value AS DECIMAL(10,2)))
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_services ON p.ID = pm_services.post_id
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
WHERE p.post_type = %s
AND p.post_status = 'publish'
AND pm_services.meta_key = '_bnb_booking_services_total'
AND pm_in.meta_key = '_bnb_booking_check_in'
AND pm_in.meta_value >= %s
AND pm_status.meta_key = '_bnb_booking_status'
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
Booking::POST_TYPE,
$first_of_month
)
);
return round( (float) $result, 2 );
}
}

View File

@@ -19,6 +19,7 @@ use Magdev\WpBnb\Booking\EmailNotifier;
use Magdev\WpBnb\Frontend\Search; use Magdev\WpBnb\Frontend\Search;
use Magdev\WpBnb\Frontend\Shortcodes; use Magdev\WpBnb\Frontend\Shortcodes;
use Magdev\WpBnb\Integration\CF7; use Magdev\WpBnb\Integration\CF7;
use Magdev\WpBnb\Integration\Prometheus;
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar; use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms; use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms; use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
@@ -142,6 +143,9 @@ final class Plugin {
CF7::init(); CF7::init();
} }
// Initialize Prometheus metrics integration.
Prometheus::init();
// Initialize admin components. // Initialize admin components.
if ( is_admin() ) { if ( is_admin() ) {
$this->init_admin(); $this->init_admin();
@@ -610,6 +614,10 @@ final class Plugin {
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>"> class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?> <?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
</a> </a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=metrics' ) ); ?>"
class="nav-tab <?php echo 'metrics' === $active_tab ? 'nav-tab-active' : ''; ?>">
<?php esc_html_e( 'Metrics', 'wp-bnb' ); ?>
</a>
</nav> </nav>
<div class="tab-content"> <div class="tab-content">
@@ -624,6 +632,9 @@ final class Plugin {
case 'updates': case 'updates':
$this->render_updates_settings(); $this->render_updates_settings();
break; break;
case 'metrics':
$this->render_metrics_settings();
break;
default: default:
$this->render_general_settings(); $this->render_general_settings();
break; break;
@@ -1265,6 +1276,163 @@ final class Plugin {
<?php <?php
} }
/**
* Render metrics settings tab.
*
* @return void
*/
private function render_metrics_settings(): void {
$metrics_enabled = Prometheus::is_enabled();
$prometheus_active = class_exists( '\Magdev\WpPrometheus\Plugin' ) || defined( 'WP_PROMETHEUS_VERSION' );
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
$dashboard_available = file_exists( $dashboard_file );
?>
<form method="post" action="">
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
<h2><?php esc_html_e( 'Prometheus Metrics', 'wp-bnb' ); ?></h2>
<?php if ( ! $prometheus_active ) : ?>
<div class="notice notice-warning inline" style="margin: 0 0 20px 0;">
<p>
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
<strong><?php esc_html_e( 'WP Prometheus not detected', 'wp-bnb' ); ?></strong><br>
<?php
printf(
/* translators: %s: Plugin URL */
esc_html__( 'The WP Prometheus plugin is required to expose metrics. Please install and activate it from %s.', 'wp-bnb' ),
'<a href="https://src.bundespruefstelle.ch/magdev/wp-prometheus" target="_blank">wp-prometheus</a>'
);
?>
</p>
</div>
<?php else : ?>
<div class="notice notice-success inline" style="margin: 0 0 20px 0;">
<p>
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
<strong><?php esc_html_e( 'WP Prometheus is active', 'wp-bnb' ); ?></strong>
<?php esc_html_e( 'Metrics will be exposed via the /metrics/ endpoint.', 'wp-bnb' ); ?>
</p>
</div>
<?php endif; ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Enable Metrics', 'wp-bnb' ); ?></th>
<td>
<label>
<input type="checkbox" name="wp_bnb_metrics_enabled"
value="yes" <?php checked( $metrics_enabled ); ?>>
<?php esc_html_e( 'Expose BnB metrics via Prometheus', 'wp-bnb' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'When enabled, occupancy, booking, revenue, and guest metrics will be available for Prometheus scraping.', 'wp-bnb' ); ?>
</p>
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Available Metrics', 'wp-bnb' ); ?></h2>
<p class="description"><?php esc_html_e( 'The following metrics are exposed when enabled:', 'wp-bnb' ); ?></p>
<table class="widefat striped" style="max-width: 800px; margin-top: 10px;">
<thead>
<tr>
<th><?php esc_html_e( 'Metric', 'wp-bnb' ); ?></th>
<th><?php esc_html_e( 'Type', 'wp-bnb' ); ?></th>
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>wp_bnb_buildings_total</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Total number of buildings', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_rooms_total</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Total rooms by status (available, occupied, maintenance, inactive)', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_bookings_total</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Total bookings by status (pending, confirmed, checked_in, checked_out, cancelled)', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_occupancy_rate_current</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Current room occupancy rate (percentage)', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_occupancy_rate_this_month</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Room occupancy rate for current month (percentage)', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_checkins_today</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Number of check-ins scheduled for today', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_checkouts_today</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Number of check-outs scheduled for today', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_revenue_this_month</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Total revenue for current month', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_revenue_ytd</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Total revenue year to date', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_booking_avg_value</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Average booking value', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_guests_total</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Total number of registered guests', 'wp-bnb' ); ?></td>
</tr>
<tr>
<td><code>wp_bnb_guests_repeat</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
<td><?php esc_html_e( 'Number of repeat guests (more than one booking)', 'wp-bnb' ); ?></td>
</tr>
</tbody>
</table>
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Grafana Dashboard', 'wp-bnb' ); ?></h2>
<?php if ( $dashboard_available ) : ?>
<p>
<?php esc_html_e( 'A pre-configured Grafana dashboard is available for visualizing WP BnB metrics.', 'wp-bnb' ); ?>
</p>
<p>
<strong><?php esc_html_e( 'Dashboard file:', 'wp-bnb' ); ?></strong>
<code>assets/grafana/wp-bnb-dashboard.json</code>
</p>
<p class="description">
<?php esc_html_e( 'If WP Prometheus is installed, this dashboard will be automatically registered and available for export in the Prometheus settings.', 'wp-bnb' ); ?>
</p>
<?php else : ?>
<div class="notice notice-warning inline">
<p><?php esc_html_e( 'Grafana dashboard file not found.', 'wp-bnb' ); ?></p>
</div>
<?php endif; ?>
<p class="submit">
<?php submit_button( __( 'Save Metrics Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
</p>
</form>
<?php
}
/** /**
* Render license status badge. * Render license status badge.
* *
@@ -1336,6 +1504,9 @@ final class Plugin {
case 'updates': case 'updates':
$this->save_updates_settings(); $this->save_updates_settings();
break; break;
case 'metrics':
$this->save_metrics_settings();
break;
default: default:
$this->save_general_settings(); $this->save_general_settings();
break; break;
@@ -1465,6 +1636,19 @@ final class Plugin {
settings_errors( 'wp_bnb_settings' ); settings_errors( 'wp_bnb_settings' );
} }
/**
* Save metrics settings.
*
* @return void
*/
private function save_metrics_settings(): void {
$metrics_enabled = isset( $_POST['wp_bnb_metrics_enabled'] ) ? 'yes' : 'no';
update_option( Prometheus::OPTION_ENABLED, $metrics_enabled );
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Metrics settings saved.', 'wp-bnb' ), 'success' );
settings_errors( 'wp_bnb_settings' );
}
/** /**
* AJAX handler for checking room availability. * AJAX handler for checking room availability.
* *

View File

@@ -3,7 +3,7 @@
* Plugin Name: WP BnB Management * Plugin Name: WP BnB Management
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb * Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests. * Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
* Version: 0.8.0 * Version: 0.9.0
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 8.3 * Requires PHP: 8.3
* Author: Marco Graetsch * Author: Marco Graetsch
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
// Plugin version constant - MUST match Version in header above. // Plugin version constant - MUST match Version in header above.
define( 'WP_BNB_VERSION', '0.8.0' ); define( 'WP_BNB_VERSION', '0.9.0' );
// Plugin path constants. // Plugin path constants.
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) ); define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );