Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cda896ebe0 | ||
|
|
c439194c18 | ||
|
|
bf7c0d7d61 |
6 changed files with 211 additions and 206 deletions
|
|
@ -10,93 +10,56 @@ return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Agentic module tables - AI agents, tasks, sessions.
|
* Agentic module tables - AI agents, tasks, sessions.
|
||||||
|
*
|
||||||
|
* Guarded with hasTable() so this migration is idempotent and
|
||||||
|
* can coexist with the consolidated app-level migration.
|
||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::disableForeignKeyConstraints();
|
Schema::disableForeignKeyConstraints();
|
||||||
|
|
||||||
// 1. Agent API Keys
|
if (! Schema::hasTable('agent_api_keys')) {
|
||||||
Schema::create('agent_api_keys', function (Blueprint $table) {
|
Schema::create('agent_api_keys', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->uuid('uuid')->unique();
|
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
$table->string('name');
|
||||||
$table->string('name');
|
$table->string('key');
|
||||||
$table->string('key_hash', 64)->unique();
|
$table->json('permissions')->nullable();
|
||||||
$table->string('key_prefix', 12);
|
$table->unsignedInteger('rate_limit')->nullable();
|
||||||
$table->json('allowed_agents')->nullable();
|
$table->unsignedBigInteger('call_count')->default(0);
|
||||||
$table->json('rate_limits')->nullable();
|
$table->timestamp('last_used_at')->nullable();
|
||||||
$table->boolean('is_active')->default(true);
|
$table->timestamp('expires_at')->nullable();
|
||||||
$table->timestamp('expires_at')->nullable();
|
$table->timestamp('revoked_at')->nullable();
|
||||||
$table->timestamp('last_used_at')->nullable();
|
$table->timestamps();
|
||||||
$table->unsignedBigInteger('usage_count')->default(0);
|
|
||||||
$table->timestamps();
|
|
||||||
$table->softDeletes();
|
|
||||||
|
|
||||||
$table->index(['workspace_id', 'is_active']);
|
$table->index('workspace_id');
|
||||||
$table->index('key_prefix');
|
$table->index('key');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Agent Tasks
|
if (! Schema::hasTable('agent_sessions')) {
|
||||||
Schema::create('agent_tasks', function (Blueprint $table) {
|
Schema::create('agent_sessions', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->uuid('uuid')->unique();
|
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
$table->foreignId('agent_api_key_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
$table->string('session_id')->unique();
|
||||||
$table->foreignId('api_key_id')->nullable()->constrained('agent_api_keys')->nullOnDelete();
|
$table->string('agent_type')->nullable();
|
||||||
$table->string('agent_type');
|
$table->string('status')->default('active');
|
||||||
$table->string('status', 32)->default('pending');
|
$table->json('context_summary')->nullable();
|
||||||
$table->text('prompt');
|
$table->json('work_log')->nullable();
|
||||||
$table->json('context')->nullable();
|
$table->json('artifacts')->nullable();
|
||||||
$table->json('result')->nullable();
|
$table->json('handoff_notes')->nullable();
|
||||||
$table->json('tool_calls')->nullable();
|
$table->text('final_summary')->nullable();
|
||||||
$table->unsignedInteger('input_tokens')->default(0);
|
$table->timestamp('started_at')->nullable();
|
||||||
$table->unsignedInteger('output_tokens')->default(0);
|
$table->timestamp('last_active_at')->nullable();
|
||||||
$table->decimal('cost', 10, 6)->default(0);
|
$table->timestamp('ended_at')->nullable();
|
||||||
$table->unsignedInteger('duration_ms')->nullable();
|
$table->timestamps();
|
||||||
$table->text('error_message')->nullable();
|
|
||||||
$table->timestamp('started_at')->nullable();
|
|
||||||
$table->timestamp('completed_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['workspace_id', 'status']);
|
$table->index('workspace_id');
|
||||||
$table->index(['agent_type', 'status']);
|
$table->index('status');
|
||||||
$table->index('created_at');
|
$table->index('agent_type');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// 3. Agent Sessions
|
|
||||||
Schema::create('agent_sessions', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->uuid('uuid')->unique();
|
|
||||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
|
||||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
|
||||||
$table->string('agent_type');
|
|
||||||
$table->string('status', 32)->default('active');
|
|
||||||
$table->json('context')->nullable();
|
|
||||||
$table->json('memory')->nullable();
|
|
||||||
$table->unsignedInteger('message_count')->default(0);
|
|
||||||
$table->unsignedInteger('total_tokens')->default(0);
|
|
||||||
$table->decimal('total_cost', 10, 6)->default(0);
|
|
||||||
$table->timestamp('last_activity_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['workspace_id', 'status']);
|
|
||||||
$table->index(['agent_type', 'status']);
|
|
||||||
$table->index('last_activity_at');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Agent Messages
|
|
||||||
Schema::create('agent_messages', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('session_id')->constrained('agent_sessions')->cascadeOnDelete();
|
|
||||||
$table->string('role', 32);
|
|
||||||
$table->longText('content');
|
|
||||||
$table->json('tool_calls')->nullable();
|
|
||||||
$table->json('tool_results')->nullable();
|
|
||||||
$table->unsignedInteger('tokens')->default(0);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['session_id', 'created_at']);
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::enableForeignKeyConstraints();
|
Schema::enableForeignKeyConstraints();
|
||||||
}
|
}
|
||||||
|
|
@ -104,9 +67,7 @@ return new class extends Migration
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::disableForeignKeyConstraints();
|
Schema::disableForeignKeyConstraints();
|
||||||
Schema::dropIfExists('agent_messages');
|
|
||||||
Schema::dropIfExists('agent_sessions');
|
Schema::dropIfExists('agent_sessions');
|
||||||
Schema::dropIfExists('agent_tasks');
|
|
||||||
Schema::dropIfExists('agent_api_keys');
|
Schema::dropIfExists('agent_api_keys');
|
||||||
Schema::enableForeignKeyConstraints();
|
Schema::enableForeignKeyConstraints();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,43 @@ return new class extends Migration
|
||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
|
if (! Schema::hasTable('agent_api_keys')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Schema::table('agent_api_keys', function (Blueprint $table) {
|
Schema::table('agent_api_keys', function (Blueprint $table) {
|
||||||
$table->boolean('ip_restriction_enabled')->default(false)->after('rate_limits');
|
if (! Schema::hasColumn('agent_api_keys', 'ip_restriction_enabled')) {
|
||||||
$table->json('ip_whitelist')->nullable()->after('ip_restriction_enabled');
|
$table->boolean('ip_restriction_enabled')->default(false);
|
||||||
$table->string('last_used_ip', 45)->nullable()->after('last_used_at');
|
}
|
||||||
|
if (! Schema::hasColumn('agent_api_keys', 'ip_whitelist')) {
|
||||||
|
$table->json('ip_whitelist')->nullable();
|
||||||
|
}
|
||||||
|
if (! Schema::hasColumn('agent_api_keys', 'last_used_ip')) {
|
||||||
|
$table->string('last_used_ip', 45)->nullable();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
|
if (! Schema::hasTable('agent_api_keys')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Schema::table('agent_api_keys', function (Blueprint $table) {
|
Schema::table('agent_api_keys', function (Blueprint $table) {
|
||||||
$table->dropColumn(['ip_restriction_enabled', 'ip_whitelist', 'last_used_ip']);
|
$cols = [];
|
||||||
|
if (Schema::hasColumn('agent_api_keys', 'ip_restriction_enabled')) {
|
||||||
|
$cols[] = 'ip_restriction_enabled';
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('agent_api_keys', 'ip_whitelist')) {
|
||||||
|
$cols[] = 'ip_whitelist';
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('agent_api_keys', 'last_used_ip')) {
|
||||||
|
$cols[] = 'last_used_ip';
|
||||||
|
}
|
||||||
|
if ($cols) {
|
||||||
|
$table->dropColumn($cols);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,82 +11,76 @@ return new class extends Migration
|
||||||
/**
|
/**
|
||||||
* Create agent plans, phases, and workspace states tables.
|
* Create agent plans, phases, and workspace states tables.
|
||||||
*
|
*
|
||||||
* These tables support the structured work plan system that enables
|
* Guarded with hasTable() so this migration is idempotent and
|
||||||
* multi-agent handoff and context recovery across sessions.
|
* can coexist with the consolidated app-level migration.
|
||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::disableForeignKeyConstraints();
|
Schema::disableForeignKeyConstraints();
|
||||||
|
|
||||||
// 1. Agent Plans - structured work plans with phases
|
if (! Schema::hasTable('agent_plans')) {
|
||||||
Schema::create('agent_plans', function (Blueprint $table) {
|
Schema::create('agent_plans', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->string('slug')->unique();
|
$table->string('slug')->unique();
|
||||||
$table->string('title');
|
$table->string('title');
|
||||||
$table->text('description')->nullable();
|
$table->text('description')->nullable();
|
||||||
$table->longText('context')->nullable();
|
$table->longText('context')->nullable();
|
||||||
$table->json('phases')->nullable(); // Deprecated: use agent_phases table
|
$table->json('phases')->nullable();
|
||||||
$table->string('status', 32)->default('draft');
|
$table->string('status', 32)->default('draft');
|
||||||
$table->string('current_phase')->nullable();
|
$table->string('current_phase')->nullable();
|
||||||
$table->json('metadata')->nullable();
|
$table->json('metadata')->nullable();
|
||||||
$table->string('source_file')->nullable();
|
$table->string('source_file')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index(['workspace_id', 'status']);
|
$table->index(['workspace_id', 'status']);
|
||||||
$table->index('slug');
|
$table->index('slug');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Agent Phases - individual phases within a plan
|
if (! Schema::hasTable('agent_phases')) {
|
||||||
Schema::create('agent_phases', function (Blueprint $table) {
|
Schema::create('agent_phases', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||||
$table->unsignedInteger('order')->default(0);
|
$table->unsignedInteger('order')->default(0);
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->text('description')->nullable();
|
$table->text('description')->nullable();
|
||||||
$table->json('tasks')->nullable();
|
$table->json('tasks')->nullable();
|
||||||
$table->json('dependencies')->nullable();
|
$table->json('dependencies')->nullable();
|
||||||
$table->string('status', 32)->default('pending');
|
$table->string('status', 32)->default('pending');
|
||||||
$table->json('completion_criteria')->nullable();
|
$table->json('completion_criteria')->nullable();
|
||||||
$table->timestamp('started_at')->nullable();
|
$table->timestamp('started_at')->nullable();
|
||||||
$table->timestamp('completed_at')->nullable();
|
$table->timestamp('completed_at')->nullable();
|
||||||
$table->json('metadata')->nullable();
|
$table->json('metadata')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index(['agent_plan_id', 'order']);
|
$table->index(['agent_plan_id', 'order']);
|
||||||
$table->index(['agent_plan_id', 'status']);
|
$table->index(['agent_plan_id', 'status']);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Agent Workspace States - shared context between sessions
|
if (! Schema::hasTable('agent_workspace_states')) {
|
||||||
Schema::create('agent_workspace_states', function (Blueprint $table) {
|
Schema::create('agent_workspace_states', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||||
$table->string('key');
|
$table->string('key');
|
||||||
$table->json('value')->nullable();
|
$table->json('value')->nullable();
|
||||||
$table->string('type', 32)->default('json');
|
$table->string('type', 32)->default('json');
|
||||||
$table->text('description')->nullable();
|
$table->text('description')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique(['agent_plan_id', 'key']);
|
$table->unique(['agent_plan_id', 'key']);
|
||||||
$table->index('key');
|
$table->index('key');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Update agent_sessions to add agent_plan_id foreign key
|
// Add agent_plan_id to agent_sessions if table exists
|
||||||
// Note: Only run if agent_sessions table exists (from earlier migration)
|
|
||||||
if (Schema::hasTable('agent_sessions') && ! Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
if (Schema::hasTable('agent_sessions') && ! Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
||||||
Schema::table('agent_sessions', function (Blueprint $table) {
|
Schema::table('agent_sessions', function (Blueprint $table) {
|
||||||
$table->foreignId('agent_plan_id')
|
$table->foreignId('agent_plan_id')
|
||||||
->nullable()
|
->nullable()
|
||||||
->after('workspace_id')
|
|
||||||
->constrained('agent_plans')
|
->constrained('agent_plans')
|
||||||
->nullOnDelete();
|
->nullOnDelete();
|
||||||
$table->json('context_summary')->nullable()->after('context');
|
|
||||||
$table->json('work_log')->nullable()->after('context_summary');
|
|
||||||
$table->json('artifacts')->nullable()->after('work_log');
|
|
||||||
$table->json('handoff_notes')->nullable()->after('artifacts');
|
|
||||||
$table->text('final_summary')->nullable()->after('handoff_notes');
|
|
||||||
$table->timestamp('started_at')->nullable()->after('final_summary');
|
|
||||||
$table->timestamp('ended_at')->nullable()->after('started_at');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,20 +91,10 @@ return new class extends Migration
|
||||||
{
|
{
|
||||||
Schema::disableForeignKeyConstraints();
|
Schema::disableForeignKeyConstraints();
|
||||||
|
|
||||||
// Remove agent_sessions additions if table exists
|
|
||||||
if (Schema::hasTable('agent_sessions') && Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
if (Schema::hasTable('agent_sessions') && Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
||||||
Schema::table('agent_sessions', function (Blueprint $table) {
|
Schema::table('agent_sessions', function (Blueprint $table) {
|
||||||
$table->dropForeign(['agent_plan_id']);
|
$table->dropForeign(['agent_plan_id']);
|
||||||
$table->dropColumn([
|
$table->dropColumn('agent_plan_id');
|
||||||
'agent_plan_id',
|
|
||||||
'context_summary',
|
|
||||||
'work_log',
|
|
||||||
'artifacts',
|
|
||||||
'handoff_notes',
|
|
||||||
'final_summary',
|
|
||||||
'started_at',
|
|
||||||
'ended_at',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ class AgentPlan extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'context' => 'array',
|
||||||
'phases' => 'array',
|
'phases' => 'array',
|
||||||
'metadata' => 'array',
|
'metadata' => 'array',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -58,14 +58,14 @@ class Boot extends ServiceProvider implements ServiceDefinition
|
||||||
/**
|
/**
|
||||||
* Admin menu items for this service.
|
* Admin menu items for this service.
|
||||||
*
|
*
|
||||||
* Agentic is positioned in the dashboard group (not services)
|
* Agentic has its own top-level group right after Dashboard —
|
||||||
* as it's a cross-cutting AI capability, not a standalone product.
|
* this is the primary capability of the platform.
|
||||||
*/
|
*/
|
||||||
public function adminMenuItems(): array
|
public function adminMenuItems(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
'group' => 'dashboard',
|
'group' => 'agents',
|
||||||
'priority' => 5,
|
'priority' => 5,
|
||||||
'entitlement' => 'core.srv.agentic',
|
'entitlement' => 'core.srv.agentic',
|
||||||
'item' => fn () => [
|
'item' => fn () => [
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,34 @@ class Dashboard extends Component
|
||||||
public function stats(): array
|
public function stats(): array
|
||||||
{
|
{
|
||||||
return $this->cacheWithLock('admin.agents.dashboard.stats', 60, function () {
|
return $this->cacheWithLock('admin.agents.dashboard.stats', 60, function () {
|
||||||
$activePlans = AgentPlan::active()->count();
|
try {
|
||||||
$totalPlans = AgentPlan::notArchived()->count();
|
$activePlans = AgentPlan::active()->count();
|
||||||
$activeSessions = AgentSession::active()->count();
|
$totalPlans = AgentPlan::notArchived()->count();
|
||||||
$todaySessions = AgentSession::whereDate('started_at', today())->count();
|
} catch (\Throwable) {
|
||||||
|
$activePlans = 0;
|
||||||
|
$totalPlans = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Tool call stats for last 7 days
|
try {
|
||||||
$toolStats = McpToolCallStat::last7Days()
|
$activeSessions = AgentSession::active()->count();
|
||||||
->selectRaw('SUM(call_count) as total_calls')
|
$todaySessions = AgentSession::whereDate('started_at', today())->count();
|
||||||
->selectRaw('SUM(success_count) as total_success')
|
} catch (\Throwable) {
|
||||||
->first();
|
$activeSessions = 0;
|
||||||
|
$todaySessions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$toolStats = McpToolCallStat::last7Days()
|
||||||
|
->selectRaw('SUM(call_count) as total_calls')
|
||||||
|
->selectRaw('SUM(success_count) as total_success')
|
||||||
|
->first();
|
||||||
|
$totalCalls = $toolStats->total_calls ?? 0;
|
||||||
|
$totalSuccess = $toolStats->total_success ?? 0;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$totalCalls = 0;
|
||||||
|
$totalSuccess = 0;
|
||||||
|
}
|
||||||
|
|
||||||
$totalCalls = $toolStats->total_calls ?? 0;
|
|
||||||
$totalSuccess = $toolStats->total_success ?? 0;
|
|
||||||
$successRate = $totalCalls > 0 ? round(($totalSuccess / $totalCalls) * 100, 1) : 0;
|
$successRate = $totalCalls > 0 ? round(($totalSuccess / $totalCalls) * 100, 1) : 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -124,40 +139,46 @@ class Dashboard extends Component
|
||||||
return $this->cacheWithLock('admin.agents.dashboard.activity', 30, function () {
|
return $this->cacheWithLock('admin.agents.dashboard.activity', 30, function () {
|
||||||
$activities = [];
|
$activities = [];
|
||||||
|
|
||||||
// Recent plan updates
|
try {
|
||||||
$plans = AgentPlan::with('workspace')
|
$plans = AgentPlan::with('workspace')
|
||||||
->latest('updated_at')
|
->latest('updated_at')
|
||||||
->take(5)
|
->take(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
foreach ($plans as $plan) {
|
foreach ($plans as $plan) {
|
||||||
$activities[] = [
|
$activities[] = [
|
||||||
'type' => 'plan',
|
'type' => 'plan',
|
||||||
'icon' => 'clipboard-list',
|
'icon' => 'clipboard-list',
|
||||||
'title' => "Plan \"{$plan->title}\"",
|
'title' => "Plan \"{$plan->title}\"",
|
||||||
'description' => "Status: {$plan->status}",
|
'description' => "Status: {$plan->status}",
|
||||||
'workspace' => $plan->workspace?->name ?? 'Unknown',
|
'workspace' => $plan->workspace?->name ?? 'Unknown',
|
||||||
'time' => $plan->updated_at,
|
'time' => $plan->updated_at,
|
||||||
'link' => route('hub.agents.plans.show', $plan->slug),
|
'link' => route('hub.agents.plans.show', $plan->slug),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Table may not exist yet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recent sessions
|
try {
|
||||||
$sessions = AgentSession::with(['plan', 'workspace'])
|
$sessions = AgentSession::with(['plan', 'workspace'])
|
||||||
->latest('last_active_at')
|
->latest('last_active_at')
|
||||||
->take(5)
|
->take(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
foreach ($sessions as $session) {
|
foreach ($sessions as $session) {
|
||||||
$activities[] = [
|
$activities[] = [
|
||||||
'type' => 'session',
|
'type' => 'session',
|
||||||
'icon' => 'play',
|
'icon' => 'play',
|
||||||
'title' => "Session {$session->session_id}",
|
'title' => "Session {$session->session_id}",
|
||||||
'description' => $session->plan?->title ?? 'No plan',
|
'description' => $session->plan?->title ?? 'No plan',
|
||||||
'workspace' => $session->workspace?->name ?? 'Unknown',
|
'workspace' => $session->workspace?->name ?? 'Unknown',
|
||||||
'time' => $session->last_active_at ?? $session->created_at,
|
'time' => $session->last_active_at ?? $session->created_at,
|
||||||
'link' => route('hub.agents.sessions.show', $session->id),
|
'link' => route('hub.agents.sessions.show', $session->id),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Table may not exist yet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by time descending
|
// Sort by time descending
|
||||||
|
|
@ -171,7 +192,11 @@ class Dashboard extends Component
|
||||||
public function topTools(): \Illuminate\Support\Collection
|
public function topTools(): \Illuminate\Support\Collection
|
||||||
{
|
{
|
||||||
return $this->cacheWithLock('admin.agents.dashboard.toptools', 300, function () {
|
return $this->cacheWithLock('admin.agents.dashboard.toptools', 300, function () {
|
||||||
return McpToolCallStat::getTopTools(days: 7, limit: 5);
|
try {
|
||||||
|
return McpToolCallStat::getTopTools(days: 7, limit: 5);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +204,11 @@ class Dashboard extends Component
|
||||||
public function dailyTrend(): \Illuminate\Support\Collection
|
public function dailyTrend(): \Illuminate\Support\Collection
|
||||||
{
|
{
|
||||||
return $this->cacheWithLock('admin.agents.dashboard.dailytrend', 300, function () {
|
return $this->cacheWithLock('admin.agents.dashboard.dailytrend', 300, function () {
|
||||||
return McpToolCallStat::getDailyTrend(days: 7);
|
try {
|
||||||
|
return McpToolCallStat::getDailyTrend(days: 7);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,11 +216,15 @@ class Dashboard extends Component
|
||||||
public function blockedPlans(): int
|
public function blockedPlans(): int
|
||||||
{
|
{
|
||||||
return (int) $this->cacheWithLock('admin.agents.dashboard.blocked', 60, function () {
|
return (int) $this->cacheWithLock('admin.agents.dashboard.blocked', 60, function () {
|
||||||
return AgentPlan::active()
|
try {
|
||||||
->whereHas('agentPhases', function ($query) {
|
return AgentPlan::active()
|
||||||
$query->where('status', 'blocked');
|
->whereHas('agentPhases', function ($query) {
|
||||||
})
|
$query->where('status', 'blocked');
|
||||||
->count();
|
})
|
||||||
|
->count();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue