diff --git a/bavarian-rank-engine/assets/admin.css b/bavarian-rank-engine/assets/admin.css
new file mode 100644
index 0000000..23fabcc
--- /dev/null
+++ b/bavarian-rank-engine/assets/admin.css
@@ -0,0 +1,10 @@
+.bre-settings h2 {
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 5px;
+ margin-top: 30px;
+}
+.bre-provider-row { display: none; }
+.bre-provider-row.active { display: table-row; }
+.bre-test-result { margin-left: 10px; font-weight: bold; }
+.bre-test-result.success { color: #46b450; }
+.bre-test-result.error { color: #dc3232; }
diff --git a/bavarian-rank-engine/assets/admin.js b/bavarian-rank-engine/assets/admin.js
new file mode 100644
index 0000000..f806b68
--- /dev/null
+++ b/bavarian-rank-engine/assets/admin.js
@@ -0,0 +1,48 @@
+/* global breAdmin */
+jQuery( function ( $ ) {
+ function updateProviderRows() {
+ var active = $( '#bre-provider' ).val();
+ $( '.bre-provider-row' ).removeClass( 'active' );
+ $( '.bre-provider-row[data-provider="' + active + '"]' ).addClass( 'active' );
+ }
+ updateProviderRows();
+ $( '#bre-provider' ).on( 'change', updateProviderRows );
+
+ $( document ).on( 'click', '.bre-test-btn', function () {
+ var btn = $( this );
+ var providerId = btn.data( 'provider' );
+ var resultEl = $( '#test-result-' + providerId );
+
+ resultEl.removeClass( 'success error' ).text( 'Teste\u2026' );
+ btn.prop( 'disabled', true );
+
+ $.post( breAdmin.ajaxUrl, {
+ action: 'bre_test_connection',
+ nonce: breAdmin.nonce,
+ provider: providerId,
+ // api_key removed — server reads stored encrypted key
+ } ).done( function ( res ) {
+ if ( res.success ) {
+ resultEl.addClass( 'success' ).text( '\u2713 ' + res.data );
+ } else {
+ resultEl.addClass( 'error' ).text( '\u2717 ' + res.data );
+ }
+ } ).fail( function () {
+ resultEl.addClass( 'error' ).text( '\u2717 Netzwerkfehler' );
+ } ).always( function () {
+ btn.prop( 'disabled', false );
+ } );
+ } );
+
+ $( '#bre-reset-prompt' ).on( 'click', function () {
+ if ( ! confirm( 'Prompt wirklich zur\u00fccksetzen?' ) ) return;
+ $.post( breAdmin.ajaxUrl, {
+ action: 'bre_get_default_prompt',
+ nonce: breAdmin.nonce,
+ } ).done( function ( res ) {
+ if ( res.success ) {
+ $( 'textarea[name*="prompt"]' ).val( res.data );
+ }
+ } );
+ } );
+} );
diff --git a/bavarian-rank-engine/assets/bulk.js b/bavarian-rank-engine/assets/bulk.js
new file mode 100644
index 0000000..523f13b
--- /dev/null
+++ b/bavarian-rank-engine/assets/bulk.js
@@ -0,0 +1,221 @@
+/* global breBulk */
+jQuery( function ( $ ) {
+ var running = false;
+ var stopFlag = false;
+ var processed = 0;
+ var total = 0;
+ var failedItems = [];
+
+ if ( breBulk.isLocked ) {
+ showLockWarning( breBulk.lockAge );
+ }
+
+ loadStats();
+
+ function showLockWarning( age ) {
+ var msg = 'Ein Bulk-Prozess läuft bereits' + ( age ? ' (seit ' + age + 's)' : '' ) + '.';
+ $( '#bre-lock-warning' ).text( msg ).show();
+ $( '#bre-bulk-start' ).prop( 'disabled', true );
+ }
+
+ function hideLockWarning() {
+ $( '#bre-lock-warning' ).hide();
+ $( '#bre-bulk-start' ).prop( 'disabled', false );
+ }
+
+ function loadStats() {
+ $.post( breBulk.ajaxUrl, { action: 'bre_bulk_stats', nonce: breBulk.nonce } )
+ .done( function ( res ) {
+ if ( ! res.success ) return;
+ var html = 'Posts ohne Meta-Beschreibung:
';
+ var t = 0;
+ $.each( res.data, function ( pt, count ) {
+ html += '- ' + $( '' ).text( pt ).html() + ': ' + parseInt( count, 10 ) + '
';
+ t += parseInt( count, 10 );
+ } );
+ html += '
Gesamt: ' + t + '';
+ total = t;
+ $( '#bre-bulk-stats' ).html( html );
+ updateCostEstimate();
+ } );
+ }
+
+ $( '#bre-bulk-limit, #bre-bulk-model, #bre-bulk-provider' ).on( 'change', updateCostEstimate );
+
+ function updateCostEstimate() {
+ var limit = parseInt( $( '#bre-bulk-limit' ).val(), 10 ) || 20;
+ var inputTokens = limit * 800;
+ var outputTokens = limit * 50;
+ var costHtml = '~' + inputTokens + ' Input-Token + ' + outputTokens + ' Output-Token';
+
+ var costData = breBulk.costs || {};
+ var provider = $( '#bre-bulk-provider' ).val();
+ var model = $( '#bre-bulk-model' ).val();
+
+ if ( costData[ provider ] && costData[ provider ][ model ] ) {
+ var c = costData[ provider ][ model ];
+ var inCost = ( inputTokens / 1000000 ) * parseFloat( c.input || 0 );
+ var outCost= ( outputTokens / 1000000 ) * parseFloat( c.output || 0 );
+ var total = inCost + outCost;
+ if ( total > 0 ) {
+ costHtml += ' ≈ $' + total.toFixed( 4 );
+ }
+ }
+ $( '#bre-cost-estimate' ).text( costHtml );
+ }
+
+ $( '#bre-bulk-start' ).on( 'click', function () {
+ if ( running ) return;
+ $.post( breBulk.ajaxUrl, { action: 'bre_bulk_status', nonce: breBulk.nonce } )
+ .done( function ( res ) {
+ if ( res.success && res.data.locked ) {
+ showLockWarning( res.data.lock_age );
+ return;
+ }
+ startRun();
+ } );
+ } );
+
+ function startRun() {
+ running = true;
+ stopFlag = false;
+ processed = 0;
+ failedItems = [];
+
+ $( '#bre-bulk-start' ).prop( 'disabled', true );
+ $( '#bre-bulk-stop' ).show();
+ $( '#bre-progress-wrap' ).show();
+ $( '#bre-bulk-log' ).show().html( '' );
+ $( '#bre-failed-summary' ).hide().html( '' );
+ hideLockWarning();
+
+ var limit = parseInt( $( '#bre-bulk-limit' ).val(), 10 ) || 20;
+ var provider = $( '#bre-bulk-provider' ).val();
+ var model = $( '#bre-bulk-model' ).val();
+
+ log( '▶ Start — max ' + limit + ' Posts, Provider: ' + provider );
+ runBatch( 'post', limit, provider, model, true );
+ }
+
+ $( '#bre-bulk-stop' ).on( 'click', function () {
+ stopFlag = true;
+ log( '⚠ Abbruch angefordert…', 'warn' );
+ releaseLock();
+ } );
+
+ function releaseLock() {
+ $.post( breBulk.ajaxUrl, { action: 'bre_bulk_release', nonce: breBulk.nonce } );
+ }
+
+ function runBatch( postType, remaining, provider, model, isFirst ) {
+ if ( stopFlag || remaining <= 0 ) {
+ finish();
+ return;
+ }
+
+ var batchSize = Math.min( 20, remaining );
+ var isLast = ( remaining - batchSize ) <= 0;
+
+ log( '↻ Verarbeite ' + batchSize + ' Posts… (' + remaining + ' verbleibend)' );
+
+ $.post( breBulk.ajaxUrl, {
+ action: 'bre_bulk_generate',
+ nonce: breBulk.nonce,
+ post_type: postType,
+ batch_size: batchSize,
+ provider: provider,
+ model: model,
+ is_first: isFirst ? 1 : 0,
+ is_last: isLast ? 1 : 0,
+ } ).done( function ( res ) {
+ if ( ! res.success ) {
+ if ( res.data && res.data.locked ) {
+ showLockWarning( res.data.lock_age );
+ finish();
+ return;
+ }
+ log( '✗ Fehler: ' + $( '' ).text( ( res.data && res.data.message ) || 'Unbekannter Fehler' ).html(), 'error' );
+ finish();
+ return;
+ }
+
+ $.each( res.data.results, function ( i, item ) {
+ if ( item.success ) {
+ var note = item.attempts > 1 ? ' (Versuch ' + item.attempts + ')' : '';
+ log(
+ '✓ [' + item.id + '] ' +
+ $( '' ).text( item.title ).html() + note +
+ '
' +
+ $( '' ).text( item.description ).html() +
+ ''
+ );
+ } else {
+ failedItems.push( item );
+ log(
+ '✗ [' + item.id + '] ' +
+ $( '' ).text( item.title ).html() +
+ ' — ' + $( '' ).text( item.error ).html(),
+ 'error'
+ );
+ }
+ processed++;
+ } );
+
+ updateProgress( processed, total );
+
+ var newRemaining = remaining - batchSize;
+ if ( res.data.remaining > 0 && ! stopFlag && newRemaining > 0 ) {
+ setTimeout( function () {
+ runBatch( postType, newRemaining, provider, model, false );
+ }, breBulk.rateDelay );
+ } else {
+ if ( isLast || res.data.remaining === 0 ) releaseLock();
+ finish();
+ }
+ } ).fail( function () {
+ log( '✗ Netzwerkfehler', 'error' );
+ releaseLock();
+ finish();
+ } );
+ }
+
+ function updateProgress( done, t ) {
+ var pct = t > 0 ? Math.round( ( done / t ) * 100 ) : 100;
+ $( '#bre-progress-bar' ).css( 'width', pct + '%' );
+ $( '#bre-progress-text' ).text( done + ' / ' + t + ' verarbeitet' );
+ }
+
+ /**
+ * Append a line to the log console.
+ * @param {string} msg Pre-escaped HTML string. User data MUST be escaped via
+ * $('').text(val).html() before passing here.
+ * @param {string} type 'error' | 'warn' | undefined
+ */
+ function log( msg, type ) {
+ var color = type === 'error' ? '#f48771' : type === 'warn' ? '#dcdcaa' : '#9cdcfe';
+ $( '#bre-bulk-log' ).append(
+ '' + msg + '
'
+ );
+ var el = document.getElementById( 'bre-bulk-log' );
+ el.scrollTop = el.scrollHeight;
+ }
+
+ function finish() {
+ running = false;
+ $( '#bre-bulk-start' ).prop( 'disabled', false );
+ $( '#bre-bulk-stop' ).hide();
+ log( '— Fertig —' );
+
+ if ( failedItems.length > 0 ) {
+ var html = '⚠ ' + failedItems.length + ' Posts fehlgeschlagen:';
+ $.each( failedItems, function ( i, item ) {
+ html += '- [' + item.id + '] ' +
+ $( '' ).text( item.title ).html() +
+ ': ' + $( '' ).text( item.error ).html() + '
';
+ } );
+ html += '
';
+ $( '#bre-failed-summary' ).html( html ).show();
+ }
+ loadStats();
+ }
+} );
diff --git a/bavarian-rank-engine/assets/editor-meta.js b/bavarian-rank-engine/assets/editor-meta.js
new file mode 100644
index 0000000..5da315c
--- /dev/null
+++ b/bavarian-rank-engine/assets/editor-meta.js
@@ -0,0 +1,32 @@
+/* global jQuery, ajaxurl */
+jQuery( function ( $ ) {
+ var $textarea = $( '#bre-meta-description' );
+ var $count = $( '#bre-meta-count' );
+ var $btn = $( '#bre-regen-meta' );
+
+ if ( ! $textarea.length ) return;
+
+ $textarea.on( 'input', function () {
+ $count.text( $( this ).val().length + ' / 160' );
+ } );
+
+ if ( ! $btn.length ) return;
+
+ $btn.on( 'click', function () {
+ $btn.prop( 'disabled', true ).text( '…' );
+ $.post( ajaxurl, {
+ action: 'bre_regen_meta',
+ nonce: $btn.data( 'nonce' ),
+ post_id: $btn.data( 'post-id' ),
+ } ).done( function ( res ) {
+ if ( res.success ) {
+ $textarea.val( res.data.description );
+ $count.text( res.data.description.length + ' / 160' );
+ } else {
+ alert( 'Fehler: ' + ( res.data || 'Unbekannt' ) );
+ }
+ } ).always( function () {
+ $btn.prop( 'disabled', false ).text( 'Mit KI neu generieren' );
+ } );
+ } );
+} );
diff --git a/bavarian-rank-engine/assets/seo-widget.js b/bavarian-rank-engine/assets/seo-widget.js
new file mode 100644
index 0000000..78da5c4
--- /dev/null
+++ b/bavarian-rank-engine/assets/seo-widget.js
@@ -0,0 +1,102 @@
+/* global jQuery, wp */
+jQuery( function ( $ ) {
+ var $widget = $( '#bre-seo-widget' );
+ if ( ! $widget.length ) return;
+
+ var siteUrl = $widget.data( 'site-url' ) || window.location.origin;
+ var debounce = null;
+
+ function getContent() {
+ // Block editor
+ if ( window.wp && wp.data && wp.data.select( 'core/editor' ) ) {
+ try {
+ var blocks = wp.data.select( 'core/editor' ).getBlocks();
+ return blocks.map( function ( b ) {
+ return ( b.attributes && b.attributes.content ) ? b.attributes.content : '';
+ } ).join( ' ' );
+ } catch ( e ) { return ''; }
+ }
+ // Classic editor (TinyMCE or textarea)
+ if ( typeof tinyMCE !== 'undefined' && tinyMCE.activeEditor && ! tinyMCE.activeEditor.isHidden() ) {
+ return tinyMCE.activeEditor.getContent();
+ }
+ return $( '#content' ).val() || '';
+ }
+
+ function getTitle() {
+ if ( window.wp && wp.data && wp.data.select( 'core/editor' ) ) {
+ try {
+ return wp.data.select( 'core/editor' ).getEditedPostAttribute( 'title' ) || '';
+ } catch ( e ) { return ''; }
+ }
+ return $( '#title' ).val() || '';
+ }
+
+ function analyse() {
+ var content = getContent();
+ var title = getTitle();
+ var plain = content.replace( /<[^>]+>/g, ' ' ).replace( /\s+/g, ' ' ).trim();
+ var words = plain ? plain.split( /\s+/ ).length : 0;
+ var readMin = Math.max( 1, Math.ceil( words / 200 ) );
+
+ $( '#bre-title-stat' ).text( title.length + ' / 60' );
+ $( '#bre-words-stat' ).text( words.toLocaleString( 'de-DE' ) );
+ $( '#bre-read-stat' ).text( '~' + readMin + ' Min.' );
+
+ // Headings — count from HTML tags
+ var h = { h1: 0, h2: 0, h3: 0, h4: 0 };
+ ( content.match( /]/gi ) || [] ).forEach( function ( tag ) {
+ var level = 'h' + tag.replace( / 0 ) hParts.push( h[ tag ] + '× ' + tag.toUpperCase() );
+ } );
+ $( '#bre-headings-stat' ).text( hParts.length ? hParts.join( ' ' ) : 'Keine' );
+
+ // Links
+ var allLinks = content.match( /href="([^"]+)"/gi ) || [];
+ var siteHost = siteUrl.replace( /https?:\/\//, '' ).replace( /\/$/, '' );
+ var internal = 0;
+ var external = 0;
+
+ allLinks.forEach( function ( tag ) {
+ var href = ( tag.match( /href="([^"]+)"/ ) || [] )[1] || '';
+ if ( href.indexOf( '/' ) === 0 || href.indexOf( siteUrl ) === 0 || href.indexOf( siteHost ) !== -1 ) {
+ internal++;
+ } else if ( /^https?:\/\//.test( href ) ) {
+ external++;
+ }
+ } );
+
+ $( '#bre-links-stat' ).text( internal + ' intern ' + external + ' extern' );
+
+ // Warnings
+ var warnings = [];
+ if ( h.h1 === 0 ) warnings.push( '⚠ Keine H1-Überschrift' );
+ if ( h.h1 > 1 ) warnings.push( '⚠ Mehrere H1-Überschriften (' + h.h1 + ')' );
+ if ( internal === 0 && words > 50 ) warnings.push( '⚠ Keine internen Links' );
+ $( '#bre-seo-warnings' ).html( warnings.join( '
' ) );
+ }
+
+ function scheduledAnalyse() {
+ clearTimeout( debounce );
+ debounce = setTimeout( analyse, 500 );
+ }
+
+ // Block editor
+ if ( window.wp && wp.data ) {
+ wp.data.subscribe( scheduledAnalyse );
+ }
+
+ // Classic editor
+ $( document ).on( 'input change', '#content', scheduledAnalyse );
+ $( document ).on( 'tinymce-editor-init', function ( event, editor ) {
+ editor.on( 'KeyUp Change SetContent', scheduledAnalyse );
+ } );
+ $( '#title' ).on( 'input', scheduledAnalyse );
+
+ analyse();
+} );