'spfaq_faq', 'post_status' => 'publish', 'fields' => 'ids', 'no_found_rows' => true, ) ); foreach ( $ids as $id ) { $p = get_post( $id ); ++$stats[ SPFAQ_API_Mapper::sync_status( $p, SPFAQ_API_Mapper::content_hash( $p ) ) ]; } echo '
' . esc_html__( 'INTEGRATION', 'spindle-faq' ) . '

' . esc_html__( 'Knowledge API', 'spindle-faq' ) . '

' . esc_html__( 'Let companion plugins such as Spindle AI Live Chat read your published FAQs, send back unanswered questions and share helpfulness votes. Nothing unpublished or private is ever shared.', 'spindle-faq' ) . '

'; if ( SPFAQ_Request::has( 'saved' ) ) { echo '

' . esc_html__( 'Settings saved.', 'spindle-faq' ) . '

'; } if ( is_array( $shown ) ) { delete_transient( 'spfaq_api_newkey_' . get_current_user_id() ); echo '

' . esc_html( 'read' === $shown['scope'] ? __( 'New read key', 'spindle-faq' ) : __( 'New write key', 'spindle-faq' ) ) . ': ' . esc_html( $shown['key'] ) . '
' . esc_html__( 'Copy it now. It is stored only as a hash and cannot be shown again.', 'spindle-faq' ) . '

'; } echo '

' . esc_html__( 'Access', 'spindle-faq' ) . '

'; wp_nonce_field( 'spfaq_api_save' ); echo ''; echo ''; echo ''; echo '
' . esc_html__( 'Enable the API', 'spindle-faq' ) . '
' . esc_html__( 'Public read', 'spindle-faq' ) . '

' . esc_html__( 'Only already-public FAQs are returned. Leave this off to require a read key. Writes always need the write key.', 'spindle-faq' ) . '

'; echo '

' . esc_html__( 'Keys', 'spindle-faq' ) . '

' . esc_html__( 'Send a key as "Authorization: Bearer KEY". The read key can only read. The write key can send suggestions, feedback and sync reports but cannot read.', 'spindle-faq' ) . '

'; foreach ( array( 'read' => __( 'Read key', 'spindle-faq' ), 'write' => __( 'Write key', 'spindle-faq' ), ) as $scope => $label ) { $has = ! empty( $opt[ $scope . '_hash' ] ); echo ''; } echo '
' . esc_html( $label ) . '' . esc_html( $has ? __( 'Set', 'spindle-faq' ) : __( 'Not set', 'spindle-faq' ) ) . ''; foreach ( array( 'generate' => $has ? __( 'Replace', 'spindle-faq' ) : __( 'Generate', 'spindle-faq' ), 'revoke' => __( 'Revoke', 'spindle-faq' ), ) as $op => $text ) { if ( 'revoke' === $op && ! $has ) { continue; } echo '
'; wp_nonce_field( 'spfaq_api_key' ); echo '
'; } echo '
'; echo '

' . esc_html__( 'Sync status', 'spindle-faq' ) . '

'; /* translators: 1: indexed count, 2: stale count, 3: pending count, 4: excluded count */ echo esc_html( sprintf( __( 'Indexed: %1$d · Stale: %2$d · Not indexed yet: %3$d · Excluded: %4$d', 'spindle-faq' ), $stats['indexed'], $stats['stale'], $stats['pending'], $stats['excluded'] ) ); echo '

' . esc_html__( 'Companion plugins report what they indexed through the sync endpoint. A FAQ is stale when it changed after it was indexed.', 'spindle-faq' ) . '

'; echo '

' . esc_html__( 'Endpoints', 'spindle-faq' ) . '

'; $routes = array( array( 'GET', '/knowledge', __( 'Published FAQs. Parameters: page, per_page (max 100), updated_after, category.', 'spindle-faq' ) ), array( 'GET', '/knowledge/{id}', __( 'One FAQ by stable ID or numeric ID.', 'spindle-faq' ) ), array( 'GET', '/changes?since=ISO8601', __( 'FAQs changed since a time, plus removals.', 'spindle-faq' ) ), array( 'POST', '/suggestions', __( 'Body: question, source, count. Creates a draft FAQ for an editor.', 'spindle-faq' ) ), array( 'POST', '/feedback', __( 'Body: faq_id, vote (yes or no), session. Counts as a vote.', 'spindle-faq' ) ), array( 'POST', '/sync', __( 'Body: items [{id, hash}]. Reports what was indexed.', 'spindle-faq' ) ), ); foreach ( $routes as $r ) { echo ''; } echo '
' . esc_html( $r[0] ) . '' . esc_html( $r[1] ) . '' . esc_html( $r[2] ) . '

' . esc_html__( 'Base URL:', 'spindle-faq' ) . ' ' . esc_html( $base ) . '

'; echo '
'; } /** Save access settings. */ public static function save_settings() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied', 'spindle-faq' ) ); } check_admin_referer( 'spfaq_api_save' ); $opt = SPFAQ_API_Options::options(); $opt['enabled'] = ! empty( $_POST['enabled'] ) ? 1 : 0; $opt['public_read'] = ! empty( $_POST['public_read'] ) ? 1 : 0; update_option( SPFAQ_API_Options::OPTION, $opt, false ); wp_safe_redirect( admin_url( 'admin.php?page=spfaq-knowledge&saved=1' ) ); exit; } /** Generate or revoke an API key. */ public static function handle_key() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied', 'spindle-faq' ) ); } check_admin_referer( 'spfaq_api_key' ); $scope = isset( $_POST['scope'] ) && 'write' === $_POST['scope'] ? 'write' : 'read'; $op = isset( $_POST['op'] ) && 'revoke' === $_POST['op'] ? 'revoke' : 'generate'; $opt = SPFAQ_API_Options::options(); if ( 'revoke' === $op ) { $opt[ $scope . '_hash' ] = ''; } else { $key = ( 'read' === $scope ? 'spfk_r_' : 'spfk_w_' ) . wp_generate_password( 40, false, false ); $opt[ $scope . '_hash' ] = SPFAQ_API_Auth::hash_key( $key ); $opt[ $scope . '_created' ] = time(); set_transient( 'spfaq_api_newkey_' . get_current_user_id(), array( 'scope' => $scope, 'key' => $key, ), 5 * MINUTE_IN_SECONDS ); } update_option( SPFAQ_API_Options::OPTION, $opt, false ); wp_safe_redirect( admin_url( 'admin.php?page=spfaq-knowledge' ) ); exit; } }