This commit is contained in:
46
app/Bots/BashScript.php
Normal file
46
app/Bots/BashScript.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bots;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class BashScript implements BotContract
|
||||
{
|
||||
protected array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if (!empty($this->config['script'])) {
|
||||
// Execute the bash script
|
||||
$script = $this->config['script'];
|
||||
$output = [];
|
||||
$returnVar = null;
|
||||
exec($script, $output, $returnVar);
|
||||
|
||||
if ($returnVar !== 0) {
|
||||
// Log error if the script failed
|
||||
\Illuminate\Support\Facades\Log::error("Bash script execution failed: " . implode("\n", $output));
|
||||
} else {
|
||||
Log::info("Bash script executed successfully: " . implode("\n", $output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function configSchema(): array
|
||||
{
|
||||
return [
|
||||
'script' => [
|
||||
'type' => 'textarea',
|
||||
'label' => 'Bash Script',
|
||||
'rules' => [
|
||||
'required',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$bots = \App\Models\Bot::all();
|
||||
$bots = Auth::user()->bots;
|
||||
|
||||
return view('dashboard', compact('bots'));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Bot;
|
||||
use App\Models\BotLog;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
@@ -13,9 +14,7 @@ class RunBot implements ShouldQueue
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(private Bot $bot)
|
||||
{
|
||||
}
|
||||
public function __construct(private Bot $bot, private BotLog $log) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
@@ -24,6 +23,22 @@ class RunBot implements ShouldQueue
|
||||
{
|
||||
$class = new $this->bot->class($this->bot->config ?? []);
|
||||
|
||||
try {
|
||||
$class->run();
|
||||
|
||||
// Update the log entry on success
|
||||
$this->log->update([
|
||||
'finished_at' => now(),
|
||||
'status' => 'success',
|
||||
// 'output' => is_string($result) ? $result : json_encode($result, JSON_PRETTY_PRINT),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// Log the error in the bot log
|
||||
$this->log->update([
|
||||
'finished_at' => now(),
|
||||
'status' => 'failed',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
app/Livewire/BotLogs.php
Normal file
35
app/Livewire/BotLogs.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\BotLog;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithoutUrlPagination;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class BotLogs extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
use WithoutUrlPagination;
|
||||
|
||||
// public function mount()
|
||||
// {
|
||||
// $this->getLogs();
|
||||
// }
|
||||
|
||||
// public function getLogs(): void
|
||||
// {
|
||||
// $this->logs = Auth::user()->bots->pluck('logs')->flatten()->sortByDesc('created_at');
|
||||
// }
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.bot-logs', [
|
||||
'logs' => BotLog::whereHas('bot', fn($q) => $q->where('user_id', Auth::id()))
|
||||
->orderBy('created_at', 'desc')
|
||||
->latest()
|
||||
->paginate(10),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,21 @@ class BotsList extends Component
|
||||
{
|
||||
public $bots;
|
||||
|
||||
public string $query = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->bots = Auth::user()->bots;
|
||||
}
|
||||
|
||||
public function search()
|
||||
{
|
||||
$this->bots = Auth::user()->bots()
|
||||
->where('name', 'like', '%' . $this->query . '%')
|
||||
->orWhere('class', 'like', '%' . $this->query . '%')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function toggleBot($botId)
|
||||
{
|
||||
$bot = \App\Models\Bot::find($botId);
|
||||
@@ -62,8 +72,13 @@ class BotsList extends Component
|
||||
|
||||
public function confirmRunBot(Bot $bot): void
|
||||
{
|
||||
$log = $bot->logs()->create([
|
||||
'status' => 'pending',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
// Dispatch the job to run the bot
|
||||
dispatch(new RunBot($bot));
|
||||
dispatch(new RunBot($bot, $log));
|
||||
|
||||
flash()->success("Bot '{$bot->name}' is being executed.");
|
||||
}
|
||||
|
||||
38
app/Livewire/ViewBot.php
Normal file
38
app/Livewire/ViewBot.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\RunBot;
|
||||
use App\Models\Bot;
|
||||
use Jantinnerezo\LivewireAlert\Facades\LivewireAlert;
|
||||
use Livewire\Component;
|
||||
|
||||
class ViewBot extends Component
|
||||
{
|
||||
public Bot $bot;
|
||||
|
||||
public function runBot()
|
||||
{
|
||||
LivewireAlert::title('Are you sure you want to run ' . $this->bot->name . '?')
|
||||
->asConfirm()
|
||||
->onConfirm('confirmRunBot')
|
||||
->show();
|
||||
}
|
||||
|
||||
public function confirmRunBot(): void
|
||||
{
|
||||
$log = $this->bot->logs()->create([
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
// Dispatch the job to run the bot
|
||||
dispatch(new RunBot($this->bot, $log));
|
||||
|
||||
flash()->success("Bot '{$this->bot->name}' is being executed.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.view-bot');
|
||||
}
|
||||
}
|
||||
@@ -51,4 +51,19 @@ class Bot extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(BotLog::class);
|
||||
}
|
||||
|
||||
public function latestLog()
|
||||
{
|
||||
return $this->hasOne(BotLog::class)->latestOfMany();
|
||||
}
|
||||
|
||||
public function failedLogs()
|
||||
{
|
||||
return $this->hasMany(BotLog::class)->where('status', 'failed');
|
||||
}
|
||||
}
|
||||
|
||||
30
app/Models/BotLog.php
Normal file
30
app/Models/BotLog.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BotLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'bot_id',
|
||||
'started_at',
|
||||
'finished_at',
|
||||
'status',
|
||||
'output',
|
||||
'error',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'started_at' => 'datetime',
|
||||
'finished_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function bot()
|
||||
{
|
||||
return $this->belongsTo(Bot::class);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Bot;
|
||||
use App\Bots\BotContract;
|
||||
use App\Jobs\RunBot;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -16,14 +17,29 @@ class BotService
|
||||
foreach ($bots as $bot) {
|
||||
$cron = new CronExpression($bot->schedule);
|
||||
if ($cron->isDue()) {
|
||||
$log = $bot->logs()->create([
|
||||
'status' => 'pending',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$instance = app($bot->class, ['config' => $bot->config]);
|
||||
|
||||
if ($instance instanceof BotContract) {
|
||||
$instance->run();
|
||||
dispatch(RunBot::class, $bot, $log);
|
||||
$log->update([
|
||||
'started_at' => now(),
|
||||
'status' => 'running'
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Bot [{$bot->name}] failed: " . $e->getMessage());
|
||||
|
||||
$log->update([
|
||||
'finished_at' => now(),
|
||||
'status' => 'failed',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info("Bot [{$bot->name}] executed successfully.");
|
||||
|
||||
@@ -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('bot_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('bot_id');
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('finished_at')->nullable();
|
||||
$table->string('status')->default('pending'); // pending, running, success, failed
|
||||
$table->text('output')->nullable(); // store raw response/log text
|
||||
$table->text('error')->nullable(); // store error message/trace if failed
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bot_logs');
|
||||
}
|
||||
};
|
||||
@@ -28,13 +28,16 @@
|
||||
<div class="p-4"></div>
|
||||
</div>
|
||||
<div
|
||||
class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
class="relative aspect-video overflow-scroll rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
>
|
||||
<div class="p-4"></div>
|
||||
<div class="p-4">Logs</div>
|
||||
<div class="">
|
||||
@livewire('bot-logs')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
class="relative h-full flex-1 overflow-scroll rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
>
|
||||
<div class="p-4">
|
||||
<h2>All Bots</h2>
|
||||
|
||||
40
resources/views/livewire/bot-logs.blade.php
Normal file
40
resources/views/livewire/bot-logs.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<div>
|
||||
<table class="p-2" wire:poll.5s>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2">Bot</th>
|
||||
<th class="px-4 py-2">Status</th>
|
||||
<th class="px-4 py-2">Started At</th>
|
||||
<th class="px-4 py-2">Finished At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-center">
|
||||
@foreach ($logs as $log)
|
||||
<tr>
|
||||
<td class="px-4 py-2">
|
||||
{{ $log->bot->name }}
|
||||
</td>
|
||||
<td
|
||||
@class([
|
||||
'text-red-400' => $log->status === 'failed',
|
||||
'text-green-400' => $log->status === 'success',
|
||||
'text-yellow-400' => $log->status === 'pending',
|
||||
]),
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ $log->status }}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{{ $log->started_at ?? '' }}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{{ $log->finished_at ?? '' }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4">
|
||||
{{ $logs->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +1,27 @@
|
||||
<div>
|
||||
<table class="table w-full">
|
||||
<div class="flex gap-2 my-auto mb-4">
|
||||
<input
|
||||
|
||||
type="text"
|
||||
wire:model="query"
|
||||
wire:keydown.debounce.500ms="search"
|
||||
class="border p-2 rounded"
|
||||
placeholder="Search bots"
|
||||
/>
|
||||
</div>
|
||||
<div wire:loading wire:target.delay.longer="search">
|
||||
Searching
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<table class="table w-full" wire:loading.remove>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border px-4 py-2">Name</th>
|
||||
<th class="border px-4 py-2">Schema</th>
|
||||
<th class="border px-4 py-2">Schedule</th>
|
||||
<th class="border px-4 py-2">Next due</th>
|
||||
<th class="border px-4 py-2">Enabled</th>
|
||||
<th class="border px-4 py-2">Actions</th>
|
||||
<th class="px-4 py-2">Name</th>
|
||||
<th class="px-4 py-2">Schema</th>
|
||||
<th class="px-4 py-2">Schedule</th>
|
||||
<th class="px-4 py-2">Next due</th>
|
||||
<th class="px-4 py-2">Enabled</th>
|
||||
<th class="px-4 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-center">
|
||||
@@ -16,7 +30,7 @@
|
||||
<td class="px-4 py-2">
|
||||
<a
|
||||
wire:navigate
|
||||
href="{{ route('bots.edit', $bot) }}"
|
||||
href="{{ route('bots.show', $bot) }}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
{{ $bot->name }}
|
||||
@@ -37,7 +51,7 @@
|
||||
wire:click="toggleBot({{ $bot->id }})"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-xs">
|
||||
<button
|
||||
wire:click="runBot({{ $bot->id }})"
|
||||
class="ml-4 cursor-pointer"
|
||||
|
||||
@@ -65,10 +65,19 @@
|
||||
>
|
||||
Prettify
|
||||
</span>
|
||||
@elseif ($meta['type'] === 'textarea')
|
||||
<textarea
|
||||
wire:model.live="config.{{ $field }}"
|
||||
class="border rounded w-full"
|
||||
name="config[{{ $field }}]"
|
||||
rows="4"
|
||||
>
|
||||
{{ old("config.$field", $bot->config[$field] ?? '') }}
|
||||
</textarea>
|
||||
@else
|
||||
<input
|
||||
wire:model.live="config.{{ $field }}"
|
||||
class="border p-2 rounded mb-2 w-full"
|
||||
class="border p-2 rounded w-full"
|
||||
type="text"
|
||||
name="config[{{ $field }}]"
|
||||
value="{{ old("config.$field", $bot->config[$field] ?? '') }}"
|
||||
|
||||
22
resources/views/livewire/view-bot.blade.php
Normal file
22
resources/views/livewire/view-bot.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<div>
|
||||
<div class="flex justify-between mb-4">
|
||||
<h1 class="text-2xl font-bold mb-4">
|
||||
{{ $bot->name }}
|
||||
</h1>
|
||||
<div>
|
||||
<button class="bg-green-600 p-2 text-white rounded-lg mr-2 cursor-pointer">
|
||||
<i class="fas fa-play"></i>
|
||||
<span wire:click="confirmRunBot({{ $bot->id }})" class="cursor-pointer">
|
||||
{{ __('Run Bot') }}
|
||||
</span>
|
||||
<button
|
||||
class="bg-gray-600 p-2 text-white rounded-lg cursor-pointer"
|
||||
wire:navigate
|
||||
href="{{ route('bots.edit', $bot) }}"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
{{ __('Edit Bot') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,6 +6,7 @@ use App\Livewire\EditBot;
|
||||
use App\Livewire\Settings\Appearance;
|
||||
use App\Livewire\Settings\Password;
|
||||
use App\Livewire\Settings\Profile;
|
||||
use App\Livewire\ViewBot;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
@@ -19,6 +20,8 @@ Route::get('dashboard', [App\Http\Controllers\DashboardController::class, 'index
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('bots/create', CreateEditBot::class)
|
||||
->name('bots.create');
|
||||
Route::get('bots/{bot}', ViewBot::class)
|
||||
->name('bots.show')->middleware('can:view,bot');
|
||||
Route::get('bots/edit/{bot}', CreateEditBot::class)
|
||||
->name('bots.edit')->middleware('can:view,bot');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user