Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 105 additions & 52 deletions CLAUDE.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions wp-content/plugins/postsecret-admin/migrations/002_facets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* Migration: Replace tags with facets (topics, feelings, meanings).
*
* This migration removes the ps_tag_alias table (no longer needed)
* since facets are stored as post meta arrays.
*
* @package PostSecret\Admin
*/

namespace PostSecret\Admin\Migrations;

/**
* Run the migration.
*
* @global \wpdb $wpdb
*/
function up() {
global $wpdb;

// Drop ps_tag_alias table - no longer needed with facets
$table_name_tag_alias = $wpdb->prefix . 'ps_tag_alias';
$wpdb->query( "DROP TABLE IF EXISTS $table_name_tag_alias" );

// Note: Facets are stored as post meta:
// - _ps_topics (array)
// - _ps_feelings (array)
// - _ps_meanings (array)
// No additional tables needed - WordPress post meta handles arrays natively.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/**
* Migration: Create ps_text_embeddings table for semantic search.
*
* Stores embedding vectors for each Secret to enable:
* - Semantic similarity search
* - "Find similar" functionality
* - Topic/feeling/meaning clustering
*
* @package PostSecret\Admin
*/

namespace PostSecret\Admin\Migrations;

/**
* Run the migration.
*
* @global \wpdb $wpdb
*/
function up() {
global $wpdb;

$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'ps_text_embeddings';

$sql = "
CREATE TABLE $table_name (
secret_id bigint(20) unsigned NOT NULL,
model_version varchar(32) NOT NULL,
embedding longtext NOT NULL,
dimension smallint unsigned NOT NULL DEFAULT 1536,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (secret_id),
KEY model_version (model_version),
KEY updated_at (updated_at)
) $charset_collate;
";

require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
2 changes: 0 additions & 2 deletions wp-content/plugins/postsecret-admin/postsecret-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

use PostSecret\Admin\Routes\SearchRoute;
use PostSecret\Admin\Routes\ReviewRoute;
use PostSecret\Admin\Routes\TaxonomyRoute;
use PostSecret\Admin\Routes\BackfillRoute;
use PostSecret\Admin\Routes\SettingsRoute;

Expand All @@ -33,7 +32,6 @@
function postsecret_admin_bootstrap() {
new SearchRoute();
new ReviewRoute();
new TaxonomyRoute();
new BackfillRoute();
new SettingsRoute();
}
Expand Down
60 changes: 60 additions & 0 deletions wp-content/plugins/postsecret-admin/run-migrations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
/**
* Manual migration runner for PostSecret Admin.
*
* Run this file directly to execute all pending migrations.
* Or visit: wp-admin/admin.php?page=postsecret-run-migrations
*
* @package PostSecret\Admin
*/

// Load WordPress
// Path: /wp-content/plugins/postsecret-admin/ -> /wp-load.php
require_once __DIR__ . '/../../../wp-load.php';

if ( ! current_user_can( 'manage_options' ) && ! defined( 'WP_CLI' ) ) {
wp_die( 'Unauthorized' );
}

global $wpdb;

echo "<h1>PostSecret Database Migrations</h1>\n";

$migrations_dir = __DIR__ . '/migrations/';
$migrations = glob( $migrations_dir . '*.php' );
sort( $migrations );

foreach ( $migrations as $migration_file ) {
$migration_name = basename( $migration_file );
echo "<p>Running migration: <strong>{$migration_name}</strong>...";

// Create isolated scope and execute
$result = ( function() use ( $migration_file, $wpdb ) {
ob_start();
try {
require $migration_file;
\PostSecret\Admin\Migrations\up();
$output = ob_get_clean();
return [ 'success' => true, 'output' => $output ];
} catch ( \Throwable $e ) {
$output = ob_get_clean();
return [ 'success' => false, 'error' => $e->getMessage(), 'output' => $output ];
}
} )();

if ( $result['success'] ) {
echo " <span style='color:green;'>✓ Success</span></p>\n";
if ( ! empty( $result['output'] ) ) {
echo "<pre>" . esc_html( $result['output'] ) . "</pre>\n";
}
} else {
echo " <span style='color:red;'>✗ Failed</span></p>\n";
echo "<p style='color:red;'>Error: " . esc_html( $result['error'] ) . "</p>\n";
if ( ! empty( $result['output'] ) ) {
echo "<pre>" . esc_html( $result['output'] ) . "</pre>\n";
}
}
}

echo "<p><strong>Migrations complete!</strong></p>\n";
echo "<p><a href='" . admin_url() . "'>← Back to Dashboard</a></p>\n";
32 changes: 26 additions & 6 deletions wp-content/plugins/postsecret-admin/src/Model/Secret.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,32 @@ class Secret {
public int $id;
public string $title;
public string $content;
public array $tags = [];
public array $topics = [];
public array $feelings = [];
public array $meanings = [];

public function __construct( int $id, string $title, string $content, array $tags = [] ) {
$this->id = $id;
$this->title = $title;
$this->content = $content;
$this->tags = $tags;
public function __construct(
int $id,
string $title,
string $content,
array $topics = [],
array $feelings = [],
array $meanings = []
) {
$this->id = $id;
$this->title = $title;
$this->content = $content;
$this->topics = $topics;
$this->feelings = $feelings;
$this->meanings = $meanings;
}

/**
* Get all facets combined.
*
* @return array
*/
public function get_all_facets(): array {
return array_merge( $this->topics, $this->feelings, $this->meanings );
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,54 @@ public function unpublish( int $secret_id ): bool {
// TODO: Implement unpublish logic.
return true;
}

/**
* Update facets for a secret.
*
* @param int $secret_id Secret attachment ID.
* @param array $topics Topics array.
* @param array $feelings Feelings array.
* @param array $meanings Meanings array.
* @return bool True on success.
*/
public function update_facets( int $secret_id, array $topics = [], array $feelings = [], array $meanings = [] ): bool {
// Normalize and sort each facet array
$topics = array_values( array_filter( array_map( 'sanitize_text_field', $topics ) ) );
$feelings = array_values( array_filter( array_map( 'sanitize_text_field', $feelings ) ) );
$meanings = array_values( array_filter( array_map( 'sanitize_text_field', $meanings ) ) );

sort( $topics );
sort( $feelings );
sort( $meanings );

// Update post meta
update_post_meta( $secret_id, '_ps_topics', $topics );
update_post_meta( $secret_id, '_ps_feelings', $feelings );
update_post_meta( $secret_id, '_ps_meanings', $meanings );

// Also update in payload for consistency
$payload = get_post_meta( $secret_id, '_ps_payload', true );
if ( is_array( $payload ) ) {
$payload['topics'] = $topics;
$payload['feelings'] = $feelings;
$payload['meanings'] = $meanings;
update_post_meta( $secret_id, '_ps_payload', $payload );
}

return true;
}

/**
* Get facets for a secret.
*
* @param int $secret_id Secret attachment ID.
* @return array Facets organized by type.
*/
public function get_facets( int $secret_id ): array {
return [
'topics' => get_post_meta( $secret_id, '_ps_topics', true ) ?: [],
'feelings' => get_post_meta( $secret_id, '_ps_feelings', true ) ?: [],
'meanings' => get_post_meta( $secret_id, '_ps_meanings', true ) ?: [],
];
}
}
106 changes: 99 additions & 7 deletions wp-content/plugins/postsecret-admin/src/Services/SearchService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,105 @@ class SearchService {
/**
* Execute a search query against secrets.
*
* @param string $query Search query.
* @param array $tags Tag filters.
* @param int $page Page number.
* @return array Results.
* @param string $query Search query.
* @param array $facets Facet filters (topics, feelings, meanings).
* @param int $page Page number.
* @param int $per_page Results per page.
* @return array Results with posts and pagination info.
*/
public function search( string $query, array $tags = [], int $page = 1 ): array {
// TODO: Implement actual search logic (WP_Query or custom DB).
return [];
public function search( string $query = '', array $facets = [], int $page = 1, int $per_page = 24 ): array {
$args = [
'post_type' => 'attachment',
'post_status' => 'inherit',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
];

// Build meta query for facets
$meta_query = [];

if ( ! empty( $facets['topics'] ) ) {
$meta_query[] = [
'key' => '_ps_topics',
'value' => $facets['topics'],
'compare' => 'IN',
];
}

if ( ! empty( $facets['feelings'] ) ) {
$meta_query[] = [
'key' => '_ps_feelings',
'value' => $facets['feelings'],
'compare' => 'IN',
];
}

if ( ! empty( $facets['meanings'] ) ) {
$meta_query[] = [
'key' => '_ps_meanings',
'value' => $facets['meanings'],
'compare' => 'IN',
];
}

if ( ! empty( $meta_query ) ) {
$meta_query['relation'] = 'AND';
$args['meta_query'] = $meta_query;
}

// Add text search if query provided
if ( ! empty( $query ) ) {
$args['s'] = $query;
}

$wp_query = new \WP_Query( $args );

return [
'posts' => $wp_query->posts,
'total' => $wp_query->found_posts,
'total_pages' => $wp_query->max_num_pages,
'page' => $page,
'per_page' => $per_page,
];
}

/**
* Get all unique facet values for filtering UI.
*
* @param string $facet_type One of: topics, feelings, meanings.
* @return array Sorted unique values with counts.
*/
public function get_facet_values( string $facet_type ): array {
global $wpdb;

$meta_key = '_ps_' . $facet_type;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT meta_value, COUNT(*) as count
FROM {$wpdb->postmeta}
WHERE meta_key = %s
GROUP BY meta_value
ORDER BY count DESC, meta_value ASC",
$meta_key
)
);

$facets = [];
foreach ( $results as $row ) {
$values = maybe_unserialize( $row->meta_value );
if ( is_array( $values ) ) {
foreach ( $values as $value ) {
if ( ! isset( $facets[ $value ] ) ) {
$facets[ $value ] = 0;
}
$facets[ $value ] += (int) $row->count;
}
}
}

arsort( $facets );
return $facets;
}
}
Loading