feat: add City10 platform with Laravel 13, AI agents, 3D viewer, and LGPD compliance

Initial commit of the complete City10 virtual city simulation platform.

Backend (Laravel 13):
- REST API with Sanctum tokens and session-based web authentication
- Role-based access control (admin/user) with middleware protection
- AI agent CRUD with OpenRouter.ai integration restricted to free models
- Store management with 3D coordinates and AI shopkeeper assignment
- Versioned legal documents (Terms, Privacy, Cookies) with LGPD audit trail
- Math CAPTCHA on public forms and multi-language support (pt_BR, en, es)

Frontend:
- Responsive Blade templates with Tailwind CSS v4
- Three.js 3D city viewer with SSE chat streaming to AI agents
- Admin panel for store and user administration

Infrastructure:
- MariaDB/MySQL database with comprehensive migrations
- Database seeders for demo content and test accounts
- PHPUnit test suite and Laravel Pint code styling
- Vite build configuration with hot reload
This commit is contained in:
2026-10-17 20:46:15 -03:00
commit 79d12fa2f4
128 changed files with 17053 additions and 0 deletions
@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Adds role-based access control columns to the users table.
*
* Distinguishes administrators ("admin") from regular users ("user")
* so that API endpoints and the admin panel can be protected per role.
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
// Role used for authorization: "admin" or "user".
$table->string('role', 20)->default('user')->after('password')->index();
// Preferred UI language: pt_BR, en, or es.
$table->string('locale', 5)->default('pt_BR')->after('role');
// Soft-delete flag so accounts can be disabled without data loss.
$table->boolean('is_active')->default(true)->after('locale');
// Timestamp of the latest successful authentication (audit aid).
$table->timestamp('last_login_at')->nullable()->after('is_active');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropColumn(['role', 'locale', 'is_active', 'last_login_at']);
});
}
};
@@ -0,0 +1,70 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Creates the "ai_agents" table.
*
* Each AI agent represents an autonomous NPC or manager entity inside the
* virtual city. Decisions are delegated to models served by OpenRouter.ai,
* restricted to an allow-list of free models configured by the administrator.
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ai_agents', function (Blueprint $table): void {
$table->id();
// Human readable label, e.g. "Prefeito", "Lojista Padaria Central".
$table->string('name', 120);
// Agent archetype: mayor, shopkeeper, pedestrian, traffic_officer, etc.
$table->string('type', 40)->index();
// Free OpenRouter model identifier used for reasoning.
$table->string('model', 120)->default('deepseek/deepseek-v4-flash-0731');
// System prompt describing the agent persona and constraints.
$table->text('system_prompt')->nullable();
// Current macro goal in natural language (persisted AI state).
$table->text('current_goal')->nullable();
// JSON snapshot of the latest decision payload returned by the model.
$table->json('last_decision')->nullable();
// Operational state: active, paused, thinking, error.
$table->string('status', 20)->default('active')->index();
// Number of consecutive failures (used for backoff/circuit breaker).
$table->unsignedInteger('error_count')->default(0);
// 3D anchor position of the agent inside the city scene.
$table->decimal('pos_x', 12, 4)->default(0);
$table->decimal('pos_y', 12, 4)->default(0);
$table->decimal('pos_z', 12, 4)->default(0);
// Rotation on the Y axis (radians) for 3D rendering.
$table->decimal('rotation_y', 10, 6)->default(0);
$table->timestamp('last_think_at')->nullable();
$table->timestamps();
$table->index(['type', 'status']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('ai_agents');
}
};
@@ -0,0 +1,67 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Creates the "stores" table.
*
* Stores are physical shop buildings placed in the 3D city at an exact
* (x, y, z) coordinate and are operated by an optional AI shopkeeper agent.
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('stores', function (Blueprint $table): void {
$table->id();
// Store display name, e.g. "Padaria Central".
$table->string('name', 160);
// Free text description shown in the city UI.
$table->text('description')->nullable();
// Store category: bakery, market, hardware, restaurant, etc.
$table->string('type', 60)->index();
// Owner account (regular user acting as shopkeeper).
$table->foreignId('owner_id')->nullable()->constrained('users')->nullOnDelete();
// AI shopkeeper agent driving prices and decisions (optional).
$table->foreignId('agent_id')->nullable()->constrained('ai_agents')->nullOnDelete();
// 3D placement inside the virtual city scene.
$table->decimal('pos_x', 12, 4)->default(0);
$table->decimal('pos_y', 12, 4)->default(0);
$table->decimal('pos_z', 12, 4)->default(0);
// Building footprint dimensions in world units (used by the renderer).
$table->decimal('width', 10, 4)->default(4);
$table->decimal('height', 10, 4)->default(4);
$table->decimal('depth', 10, 4)->default(4);
// Commercial state of the store.
$table->boolean('is_open')->default(true)->index();
// Current revenue in virtual currency (updated by AI/players).
$table->decimal('revenue', 14, 2)->default(0);
$table->timestamps();
$table->index(['pos_x', 'pos_y', 'pos_z']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('stores');
}
};
@@ -0,0 +1,77 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Creates legal document and consent tables.
*
* Stores versioned legal documents (Terms of Use, Privacy Policy and Cookie
* Policy, all aligned with the Brazilian LGPD) together with an immutable
* audit trail of each user consent event.
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('legal_documents', function (Blueprint $table): void {
$table->id();
// Document kind: terms, privacy, cookies.
$table->string('type', 20)->index();
// Version identifier, e.g. "1.0.0" (incremented on edits).
$table->string('version', 20);
// Title localized per language key stored in JSON: {"pt_BR":..,"en":..,"es":..}
$table->json('title');
// Full document body per language (markdown/HTML-safe text).
$table->json('body');
// Whether this version is the one new users must accept.
$table->boolean('is_current')->default(true)->index();
$table->timestamp('published_at')->nullable();
$table->timestamps();
// A given type+version pair can only exist once.
$table->unique(['type', 'version']);
});
Schema::create('user_consents', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('legal_document_id')->constrained('legal_documents')->cascadeOnDelete();
// Moment the user accepted this specific document version.
$table->timestamp('accepted_at');
// Client IP and user agent captured at acceptance (LGPD audit).
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
// Consent channel, e.g. "web_form", "api".
$table->string('source', 30)->default('web_form');
$table->timestamps();
// One consent per user per document version (idempotent audit).
$table->unique(['user_id', 'legal_document_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_consents');
Schema::dropIfExists('legal_documents');
}
};