namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_stats' ), 'permission_callback' => array( $this, 'stats_permissions_check' ), ), ) ); // GET /stats/usage - Get daily usage breakdown. register_rest_route( $this->namespace, '/' . $this->rest_base . '/usage', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_usage' ), 'permission_callback' => array( $this, 'stats_permissions_check' ), 'args' => array( 'start_date' => array( 'description' => __( 'Start date in Y-m-d format. Defaults to 30 days ago.', 'ai-schema-gen' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'end_date' => array( 'description' => __( 'End date in Y-m-d format. Defaults to today.', 'ai-schema-gen' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), ), ), ) ); } /** * Get overall usage statistics. * * Returns aggregated counts of schemas by type and validation status, * today's AI generation count, and recent generation history. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response Response object. */ public function get_stats( $request ) { global $wpdb; error_log( 'AI Schema Gen: get_stats() called — user=' . get_current_user_id() ); $schemas_table = esc_sql( $wpdb->prefix . 'schema_ai_schemas' ); $usage_table = esc_sql( $wpdb->prefix . 'schema_ai_usage' ); $generations_table = esc_sql( $wpdb->prefix . 'schema_ai_generations' ); $today = current_time( 'Y-m-d' ); $user_id = get_current_user_id(); // Total schemas count. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $total_schemas = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT COUNT(*) FROM {$schemas_table}" ); // Schemas grouped by type. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $schemas_by_type = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT schema_type, COUNT(*) AS count FROM {$schemas_table} GROUP BY schema_type ORDER BY count DESC", ARRAY_A ); // Schemas grouped by validation status. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $schemas_by_status = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT validation_status, COUNT(*) AS count FROM {$schemas_table} GROUP BY validation_status ORDER BY count DESC", ARRAY_A ); // AI generations today for the current user. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $generations_today = (int) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT COALESCE( SUM( ai_generations ), 0 ) FROM {$usage_table} WHERE user_id = %d AND date = %s", $user_id, $today ) ); // Recent generation history (last 10 entries). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $recent_generations = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT * FROM {$generations_table} WHERE user_id = %d ORDER BY created_at DESC LIMIT %d", $user_id, 10 ), ARRAY_A ); if ( ! $recent_generations ) { $recent_generations = array(); } // Cast numeric fields and add post metadata for the recent generations. foreach ( $recent_generations as &$gen ) { $gen['id'] = isset( $gen['id'] ) ? (int) $gen['id'] : 0; $gen['user_id'] = isset( $gen['user_id'] ) ? (int) $gen['user_id'] : 0; $gen['post_id'] = isset( $gen['post_id'] ) ? (int) $gen['post_id'] : 0; $gen['confidence'] = isset( $gen['confidence'] ) ? (float) $gen['confidence'] : null; $gen['post_title'] = $gen['post_id'] ? get_the_title( $gen['post_id'] ) : ''; $gen['post_url'] = $gen['post_id'] ? get_permalink( $gen['post_id'] ) : ''; } unset( $gen ); // Cast count values. if ( $schemas_by_type ) { foreach ( $schemas_by_type as &$row ) { $row['count'] = (int) $row['count']; } unset( $row ); } else { $schemas_by_type = array(); } if ( $schemas_by_status ) { foreach ( $schemas_by_status as &$row ) { $row['count'] = (int) $row['count']; } unset( $row ); } else { $schemas_by_status = array(); } // Recent schemas (last 10 for the dashboard table). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $recent_schemas = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT id, post_id, schema_type, validation_status, updated_at AS created_at FROM {$schemas_table} ORDER BY updated_at DESC LIMIT 10", ARRAY_A ); if ( ! $recent_schemas ) { $recent_schemas = array(); } foreach ( $recent_schemas as &$schema ) { $schema['id'] = (int) $schema['id']; $schema['post_id'] = (int) $schema['post_id']; $schema['post_title'] = $schema['post_id'] ? get_the_title( $schema['post_id'] ) : ''; $schema['post_url'] = $schema['post_id'] ? get_permalink( $schema['post_id'] ) : ''; } unset( $schema ); // Build by_type and by_status as keyed objects for frontend. $by_type = array(); foreach ( $schemas_by_type as $row ) { $by_type[ $row['schema_type'] ] = $row['count']; } $by_status = array(); foreach ( $schemas_by_status as $row ) { $by_status[ $row['validation_status'] ] = $row['count']; } error_log( 'AI Schema Gen: get_stats() returning — total_schemas=' . $total_schemas . ' today_generations=' . $generations_today . ' recent_count=' . count( $recent_schemas ) ); return $this->success_response( array( 'total_schemas' => $total_schemas, 'by_type' => $by_type, 'by_status' => $by_status, 'today_generations' => $generations_today, 'recent' => $recent_schemas, 'recent_generations' => $recent_generations, ) ); } /** * Get daily usage breakdown for a date range. * * Returns a row for each day with AI generation and validation * counts from the usage table. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response Response object. */ public function get_usage( $request ) { global $wpdb; $user_id = get_current_user_id(); $end_date = sanitize_text_field( $request->get_param( 'end_date' ) ); $start_date = sanitize_text_field( $request->get_param( 'start_date' ) ); // Default to the last 30 days. if ( empty( $end_date ) ) { $end_date = current_time( 'Y-m-d' ); } if ( empty( $start_date ) ) { $start_date = gmdate( 'Y-m-d', strtotime( '-30 days', strtotime( $end_date ) ) ); } // Validate date formats. if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $start_date ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end_date ) ) { return $this->error_response( 'schema_ai_invalid_date', __( 'Dates must be in Y-m-d format.', 'ai-schema-gen' ), 400 ); } $table = esc_sql( $wpdb->prefix . 'schema_ai_usage' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $results = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT date, ai_generations, manual_generations, validations FROM {$table} WHERE user_id = %d AND date >= %s AND date <= %s ORDER BY date ASC", $user_id, $start_date, $end_date ), ARRAY_A ); if ( ! $results ) { $results = array(); } // Cast numeric fields. foreach ( $results as &$row ) { $row['ai_generations'] = (int) $row['ai_generations']; $row['manual_generations'] = (int) $row['manual_generations']; $row['validations'] = (int) $row['validations']; } unset( $row ); // Get limit/remaining from the backend — single source of truth. $settings = get_option( 'schema_ai_settings', array() ); $api_key = ! empty( $settings['api_key'] ) ? $settings['api_key'] : ''; if ( empty( $api_key ) ) { return $this->error_response( 'schema_ai_no_api_key', __( 'AI Schema Gen API key is not configured.', 'ai-schema-gen' ), 400 ); } $backend_response = wp_remote_get( SCHEMA_AI_API_BASE . '/usage', array( 'timeout' => 10, 'headers' => array( 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, ), ) ); if ( is_wp_error( $backend_response ) || 200 !== wp_remote_retrieve_response_code( $backend_response ) ) { return $this->error_response( 'schema_ai_backend_error', __( 'Could not retrieve usage from AI Schema Gen server.', 'ai-schema-gen' ), 502 ); } $backend_data = json_decode( wp_remote_retrieve_body( $backend_response ), true ); if ( empty( $backend_data['success'] ) || ! isset( $backend_data['data']['rateLimit'] ) ) { return $this->error_response( 'schema_ai_backend_error', __( 'Invalid response from AI Schema Gen server.', 'ai-schema-gen' ), 502 ); } $rate_limit = $backend_data['data']['rateLimit']; $remaining = (int) $rate_limit['remaining']; $limit = (int) $rate_limit['limit']; $monthly_used = (int) $rate_limit['used']; $plan = isset( $backend_data['data']['plan'] ) ? $backend_data['data']['plan'] : 'free'; return $this->success_response( array( 'start_date' => $start_date, 'end_date' => $end_date, 'days' => count( $results ), 'usage' => $results, 'remaining' => $remaining, 'limit' => $limit, 'used' => $monthly_used, 'plan' => $plan, ) ); } /** * Permission check for stats endpoints. * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if allowed. */ public function stats_permissions_check( $request ) { if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'schema_ai_forbidden', __( 'You do not have permission to view statistics.', 'ai-schema-gen' ), array( 'status' => 403 ) ); } return true; } }