Refactor dashboard and upload flows

Remove dead admin UI code, redesign dashboard followers/following and upload experiences, and add schema audit tooling with repair migrations for forum and upload drift.
This commit is contained in:
2026-03-21 11:02:22 +01:00
parent 29c3ff8572
commit 979e011257
55 changed files with 2576 additions and 1923 deletions

View File

@@ -0,0 +1,58 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('forum_spam_keywords')) {
Schema::create('forum_spam_keywords', function (Blueprint $table): void {
$table->id();
$table->string('keyword', 120)->unique();
$table->timestamp('created_at')->useCurrent();
});
}
if (!Schema::hasTable('forum_spam_domains')) {
Schema::create('forum_spam_domains', function (Blueprint $table): void {
$table->id();
$table->string('domain', 191)->unique();
$table->timestamp('created_at')->useCurrent();
});
}
foreach ((array) config('forum.moderation.defaults.keywords', []) as $keyword) {
$keyword = trim((string) $keyword);
if ($keyword === '') {
continue;
}
DB::table('forum_spam_keywords')->updateOrInsert(
['keyword' => $keyword],
['created_at' => now()]
);
}
foreach ((array) config('forum.moderation.defaults.domains', []) as $domain) {
$domain = strtolower(trim((string) $domain));
if ($domain === '') {
continue;
}
DB::table('forum_spam_domains')->updateOrInsert(
['domain' => $domain],
['created_at' => now()]
);
}
}
public function down(): void
{
Schema::dropIfExists('forum_spam_domains');
Schema::dropIfExists('forum_spam_keywords');
}
};

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('forum_spam_learning')) {
Schema::create('forum_spam_learning', function (Blueprint $table): void {
$table->id();
$table->string('content_hash', 64)->index();
$table->string('decision', 32)->index();
$table->string('pattern_signature', 191)->nullable()->index();
$table->foreignId('reviewed_by')->nullable()->constrained('users')->nullOnDelete();
$table->json('metadata')->nullable();
$table->timestamp('created_at')->useCurrent();
});
}
if (!Schema::hasTable('forum_ai_logs')) {
Schema::create('forum_ai_logs', function (Blueprint $table): void {
$table->id();
$table->foreignId('post_id')->constrained('forum_posts')->cascadeOnDelete();
$table->unsignedSmallInteger('ai_score')->default(0);
$table->unsignedSmallInteger('behavior_score')->default(0);
$table->unsignedSmallInteger('link_score')->default(0);
$table->integer('learning_score')->default(0);
$table->unsignedSmallInteger('firewall_score')->default(0);
$table->unsignedSmallInteger('bot_risk_score')->default(0);
$table->unsignedSmallInteger('risk_score')->default(0)->index();
$table->string('decision', 32)->default('allow')->index();
$table->string('provider', 64)->nullable()->index();
$table->string('source_ip_hash', 64)->nullable()->index();
$table->json('metadata')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['post_id', 'created_at']);
});
}
}
public function down(): void
{
Schema::dropIfExists('forum_ai_logs');
Schema::dropIfExists('forum_spam_learning');
}
};

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('forum_spam_learning')) {
Schema::create('forum_spam_learning', function (Blueprint $table): void {
$table->id();
$table->string('content_hash', 64)->index();
$table->string('decision', 32)->index();
$table->string('pattern_signature', 191)->nullable()->index();
$table->foreignId('reviewed_by')->nullable()->constrained('users')->nullOnDelete();
$table->json('metadata')->nullable();
$table->timestamp('created_at')->useCurrent();
});
}
if (!Schema::hasTable('forum_ai_logs')) {
Schema::create('forum_ai_logs', function (Blueprint $table): void {
$table->id();
$table->foreignId('post_id')->constrained('forum_posts')->cascadeOnDelete();
$table->unsignedSmallInteger('ai_score')->default(0);
$table->unsignedSmallInteger('behavior_score')->default(0);
$table->unsignedSmallInteger('link_score')->default(0);
$table->integer('learning_score')->default(0);
$table->unsignedSmallInteger('firewall_score')->default(0);
$table->unsignedSmallInteger('bot_risk_score')->default(0);
$table->unsignedSmallInteger('risk_score')->default(0)->index();
$table->string('decision', 32)->default('allow')->index();
$table->string('provider', 64)->nullable()->index();
$table->string('source_ip_hash', 64)->nullable()->index();
$table->json('metadata')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['post_id', 'created_at']);
});
}
}
public function down(): void
{
Schema::dropIfExists('forum_ai_logs');
Schema::dropIfExists('forum_spam_learning');
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('forum_tags')) {
Schema::create('forum_tags', function (Blueprint $table): void {
$table->id();
$table->string('name', 80);
$table->string('slug', 80)->unique();
$table->timestamps();
});
}
if (!Schema::hasTable('forum_topic_tags')) {
Schema::create('forum_topic_tags', function (Blueprint $table): void {
$table->id();
$table->foreignId('topic_id')->constrained('forum_topics')->cascadeOnDelete();
$table->foreignId('tag_id')->constrained('forum_tags')->cascadeOnDelete();
$table->timestamps();
$table->unique(['topic_id', 'tag_id']);
});
}
}
public function down(): void
{
Schema::dropIfExists('forum_topic_tags');
Schema::dropIfExists('forum_tags');
}
};

View File

@@ -0,0 +1,111 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('uploads')) {
$missingUploadColumns = [
'tags' => ! Schema::hasColumn('uploads', 'tags'),
'license' => ! Schema::hasColumn('uploads', 'license'),
'nsfw' => ! Schema::hasColumn('uploads', 'nsfw'),
'is_scanned' => ! Schema::hasColumn('uploads', 'is_scanned'),
'has_tags' => ! Schema::hasColumn('uploads', 'has_tags'),
'published_at' => ! Schema::hasColumn('uploads', 'published_at'),
'final_path' => ! Schema::hasColumn('uploads', 'final_path'),
];
if (in_array(true, $missingUploadColumns, true)) {
Schema::table('uploads', function (Blueprint $table) use ($missingUploadColumns): void {
if ($missingUploadColumns['tags']) {
$table->json('tags')->nullable();
}
if ($missingUploadColumns['license']) {
$table->string('license', 64)->nullable();
}
if ($missingUploadColumns['nsfw']) {
$table->boolean('nsfw')->default(false);
}
if ($missingUploadColumns['is_scanned']) {
$table->boolean('is_scanned')->default(false)->index();
}
if ($missingUploadColumns['has_tags']) {
$table->boolean('has_tags')->default(false)->index();
}
if ($missingUploadColumns['published_at']) {
$table->timestamp('published_at')->nullable()->index();
}
if ($missingUploadColumns['final_path']) {
$table->string('final_path')->nullable();
}
});
}
}
if (Schema::hasTable('forum_ai_logs')) {
$missingForumAiColumns = [
'firewall_score' => ! Schema::hasColumn('forum_ai_logs', 'firewall_score'),
'bot_risk_score' => ! Schema::hasColumn('forum_ai_logs', 'bot_risk_score'),
];
if (in_array(true, $missingForumAiColumns, true)) {
Schema::table('forum_ai_logs', function (Blueprint $table) use ($missingForumAiColumns): void {
if ($missingForumAiColumns['firewall_score']) {
$table->unsignedSmallInteger('firewall_score')->default(0);
}
if ($missingForumAiColumns['bot_risk_score']) {
$table->unsignedSmallInteger('bot_risk_score')->default(0);
}
});
}
}
}
public function down(): void
{
if (Schema::hasTable('forum_ai_logs')) {
Schema::table('forum_ai_logs', function (Blueprint $table): void {
$dropColumns = [];
if (Schema::hasColumn('forum_ai_logs', 'firewall_score')) {
$dropColumns[] = 'firewall_score';
}
if (Schema::hasColumn('forum_ai_logs', 'bot_risk_score')) {
$dropColumns[] = 'bot_risk_score';
}
if ($dropColumns !== []) {
$table->dropColumn($dropColumns);
}
});
}
if (Schema::hasTable('uploads')) {
Schema::table('uploads', function (Blueprint $table): void {
$dropColumns = [];
foreach (['tags', 'license', 'nsfw', 'is_scanned', 'has_tags', 'published_at', 'final_path'] as $column) {
if (Schema::hasColumn('uploads', $column)) {
$dropColumns[] = $column;
}
}
if ($dropColumns !== []) {
$table->dropColumn($dropColumns);
}
});
}
}
};