Compare commits
20 Commits
oskarb-pat
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cab33c16c2 | |||
| aa27851bd7 | |||
| 1565df568d | |||
| 6a72b7150b | |||
| 7764af432a | |||
| 378355ad5b | |||
| 77ebc6bce1 | |||
| 75be85e608 | |||
| ed74c14f8d | |||
| 17e2f0be35 | |||
| ed41db3017 | |||
| 7b84d09e09 | |||
| 3fb346d0a9 | |||
| eed70be12a | |||
| a5848b62ba | |||
| 39ccf02b3c | |||
| 4c07a22ff9 | |||
| 657ad8bbe6 | |||
| 819661cd9e | |||
| e608f433e9 |
@@ -6,27 +6,27 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
# - name: Checkout code
|
||||||
uses: actions/checkout@v3
|
# uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
# - name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
# uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Log in to Gitea Registry
|
# - name: Log in to Gitea Registry
|
||||||
run: |
|
# run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.oskarmikael.com -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
# echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.oskarmikael.com -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||||||
|
|
||||||
- name: Build and Push Docker image
|
# - name: Build and Push Docker image
|
||||||
run: |
|
# run: |
|
||||||
IMAGE=git.oskarmikael.com/oskarb/scheduler:latest
|
# IMAGE=git.oskarmikael.com/oskarb/scheduler:latest
|
||||||
docker buildx build \
|
# docker buildx build \
|
||||||
-f Dockerfile.prod \
|
# -f Dockerfile.prod \
|
||||||
--platform linux/amd64 \
|
# --platform linux/amd64 \
|
||||||
-t $IMAGE \
|
# -t $IMAGE \
|
||||||
--push .
|
# --push .
|
||||||
|
|
||||||
# - name: Run migrations
|
# - name: Run migrations
|
||||||
# run: |
|
# run: |
|
||||||
@@ -52,8 +52,11 @@ jobs:
|
|||||||
key: ${{ secrets.PROD_SSH_KEY }}
|
key: ${{ secrets.PROD_SSH_KEY }}
|
||||||
port: 22
|
port: 22
|
||||||
script: |
|
script: |
|
||||||
|
systemctl restart laravel-worker
|
||||||
cd /var/www/scheduler
|
cd /var/www/scheduler
|
||||||
git pull origin master
|
git pull origin master
|
||||||
/usr/lib/docker/cli-plugins/docker-compose -f docker-compose.prod.yml pull
|
npm install
|
||||||
/usr/lib/docker/cli-plugins/docker-compose -f docker-compose.prod.yml up -d
|
npm run build
|
||||||
/usr/lib/docker/cli-plugins/docker-compose -f docker-compose.prod.yml run --rm app php artisan migrate --force
|
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||||
|
php artisan optimize
|
||||||
|
php artisan migrate --force
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
# ----------------------------
|
|
||||||
# Stage 1: PHP + Composer
|
|
||||||
# ----------------------------
|
|
||||||
FROM php:8.4-fpm AS php-base
|
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
|
||||||
|
|
||||||
# Install system dependencies and PHP extensions
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
git curl libpng-dev libonig-dev libxml2-dev zip unzip \
|
|
||||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
|
|
||||||
|
|
||||||
# Install Composer
|
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
# Copy app code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Install PHP dependencies (no dev)
|
|
||||||
RUN composer install --no-dev --optimize-autoloader --no-interaction
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Stage 2: Node build
|
|
||||||
# ----------------------------
|
|
||||||
FROM node:20 AS frontend
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy only package files and install Node dependencies
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy the rest of the app code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Copy vendor from PHP stage (needed if assets reference PHP files)
|
|
||||||
COPY --from=php-base /var/www/html/vendor ./vendor
|
|
||||||
|
|
||||||
# Build assets
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Stage 3: Final production image
|
|
||||||
# ----------------------------
|
|
||||||
FROM php:8.4-fpm AS final
|
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
|
||||||
|
|
||||||
# Install system deps and PHP extensions in the final image
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
git curl libpng-dev libonig-dev libxml2-dev zip unzip \
|
|
||||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
|
|
||||||
|
|
||||||
# Copy PHP code + vendor
|
|
||||||
COPY --from=php-base /var/www/html .
|
|
||||||
|
|
||||||
# Copy built frontend assets
|
|
||||||
COPY --from=frontend /app/public ./public
|
|
||||||
|
|
||||||
# Fix permissions for Laravel
|
|
||||||
RUN chown -R www-data:www-data storage bootstrap/cache
|
|
||||||
|
|
||||||
FROM nginx:1.27-alpine AS nginx
|
|
||||||
WORKDIR /var/www/html
|
|
||||||
COPY --from=final /var/www/html/public ./public
|
|
||||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
CMD ["php-fpm"]
|
|
||||||
45
app/Bots/BashScript.php
Normal file
45
app/Bots/BashScript.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Bots;
|
||||||
|
|
||||||
|
use App\Interfaces\ScriptContract;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class BashScript implements ScriptContract
|
||||||
|
{
|
||||||
|
protected array $config;
|
||||||
|
|
||||||
|
public function __construct(array $config = [])
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): string
|
||||||
|
{
|
||||||
|
// Execute the bash script
|
||||||
|
$script = $this->config['script'];
|
||||||
|
$output = [];
|
||||||
|
$returnVar = null;
|
||||||
|
exec($script, $output, $returnVar);
|
||||||
|
|
||||||
|
|
||||||
|
if ($returnVar !== 0) {
|
||||||
|
Log::error("Bash script execution failed", ['script' => $script, 'output' => $output, 'returnVar' => $returnVar]);
|
||||||
|
throw new \RuntimeException("Bash script execution failed with return code {$returnVar}");
|
||||||
|
}
|
||||||
|
return implode("\n", $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function configSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'script' => [
|
||||||
|
'type' => 'textarea',
|
||||||
|
'label' => 'Bash Script',
|
||||||
|
'rules' => [
|
||||||
|
'required',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Bots;
|
|
||||||
|
|
||||||
interface BotContract
|
|
||||||
{
|
|
||||||
public function run(): void;
|
|
||||||
public static function configSchema(): array;
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Bots;
|
namespace App\Bots;
|
||||||
|
|
||||||
|
use App\Interfaces\BotContract;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class GenericApi implements BotContract
|
class GenericApi implements BotContract
|
||||||
{
|
{
|
||||||
@@ -17,29 +17,30 @@ class GenericApi implements BotContract
|
|||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
$this->client = new Client();
|
||||||
$this->client = new Client();
|
|
||||||
|
|
||||||
$options = [];
|
$options = [];
|
||||||
|
|
||||||
if (!empty($this->config['headers'])) {
|
if (!empty($this->config['headers'])) {
|
||||||
$options['headers'] = json_decode($this->config['headers'], true);
|
$options['headers'] = json_decode($this->config['headers'], true);
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($this->config['body'])) {
|
|
||||||
$options['json'] = json_decode($this->config['body'], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->client->request(
|
|
||||||
$this->config['method'] ?? 'GET',
|
|
||||||
$this->config['url'],
|
|
||||||
$options
|
|
||||||
);
|
|
||||||
} catch (GuzzleException $e) {
|
|
||||||
Log::error("Call to {$this->config['url']} failed" . $e->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($this->config['body'])) {
|
||||||
|
$options['json'] = json_decode($this->config['body'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->client->request(
|
||||||
|
$this->config['method'] ?? 'GET',
|
||||||
|
$this->config['url'],
|
||||||
|
$options
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => $response->getStatusCode(),
|
||||||
|
'body' => json_decode((string) $response->getBody(), true),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function configSchema(): array
|
public static function configSchema(): array
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Bots;
|
|
||||||
|
|
||||||
use App\Bots\BotContract;
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Exception\ClientException;
|
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
|
||||||
use GuzzleHttp\RequestOptions;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class Mattermost implements BotContract
|
|
||||||
{
|
|
||||||
protected Client $client;
|
|
||||||
|
|
||||||
public function __construct(private ?array $config = [])
|
|
||||||
{
|
|
||||||
$this->client = new Client([
|
|
||||||
'base_uri' => rtrim(config('scheduler.mattermost.server_url'), '/') . '/api/v4/',
|
|
||||||
'headers' => [
|
|
||||||
'Authorization' => 'Bearer ' . config('scheduler.mattermost.access_token'),
|
|
||||||
'Accept' => 'application/json',
|
|
||||||
'Content-Type' => 'application/json',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$this->client->request(
|
|
||||||
$this->config['method'] ?? 'POST',
|
|
||||||
$this->config['endpoint'],
|
|
||||||
[
|
|
||||||
'json' => json_decode($this->config['body'] ?? '', true),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (GuzzleException $e) {
|
|
||||||
Log::error("Call to Mattermost failed. " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function configSchema(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'endpoint' => [
|
|
||||||
'type' => 'string',
|
|
||||||
'label' => 'API Endpoint',
|
|
||||||
'rules' => [
|
|
||||||
'required',
|
|
||||||
'string',
|
|
||||||
'max:255',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'method' => [
|
|
||||||
'type' => 'string',
|
|
||||||
'label' => 'HTTP Method',
|
|
||||||
'default' => 'POST',
|
|
||||||
'rules' => [
|
|
||||||
'required',
|
|
||||||
'in:GET,POST,PUT,DELETE,PATCH',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'body' => [
|
|
||||||
'type' => 'json',
|
|
||||||
'label' => 'Request Body (JSON)',
|
|
||||||
'rules' => [
|
|
||||||
'nullable',
|
|
||||||
'string',
|
|
||||||
'json',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$bots = \App\Models\Bot::all();
|
$bots = Auth::user()->bots;
|
||||||
|
|
||||||
return view('dashboard', compact('bots'));
|
return view('dashboard', compact('bots'));
|
||||||
}
|
}
|
||||||
|
|||||||
7
app/Interfaces/BaseContract.php
Normal file
7
app/Interfaces/BaseContract.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Interfaces;
|
||||||
|
|
||||||
|
interface BaseContract
|
||||||
|
{
|
||||||
|
}
|
||||||
11
app/Interfaces/BotContract.php
Normal file
11
app/Interfaces/BotContract.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Interfaces;
|
||||||
|
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
interface BotContract extends BaseContract
|
||||||
|
{
|
||||||
|
public function run(): JsonResponse;
|
||||||
|
public static function configSchema(): array;
|
||||||
|
}
|
||||||
9
app/Interfaces/ScriptContract.php
Normal file
9
app/Interfaces/ScriptContract.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Interfaces;
|
||||||
|
|
||||||
|
interface ScriptContract extends BaseContract
|
||||||
|
{
|
||||||
|
public function run(): string;
|
||||||
|
public static function configSchema(): array;
|
||||||
|
}
|
||||||
51
app/Jobs/RunBot.php
Normal file
51
app/Jobs/RunBot.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Bot;
|
||||||
|
use App\Models\BotLog;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
||||||
|
class RunBot implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(private int $bot_id, private int $log_id) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$bot = Bot::findOrFail($this->bot_id);
|
||||||
|
$log = BotLog::findOrFail($this->log_id);
|
||||||
|
|
||||||
|
$log->update([
|
||||||
|
'status' => 'running',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$class = new $bot->class($bot->config ?? []);
|
||||||
|
|
||||||
|
$result = $class->run();
|
||||||
|
|
||||||
|
// Update the log entry on success
|
||||||
|
$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
|
||||||
|
$log->update([
|
||||||
|
'finished_at' => now(),
|
||||||
|
'status' => 'failed',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Livewire/BotLogs.php
Normal file
53
app/Livewire/BotLogs.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\BotLog;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Jantinnerezo\LivewireAlert\Facades\LivewireAlert;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithoutUrlPagination;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
class BotLogs extends Component
|
||||||
|
{
|
||||||
|
use WithPagination;
|
||||||
|
use WithoutUrlPagination;
|
||||||
|
|
||||||
|
public function showError(int $logId): void
|
||||||
|
{
|
||||||
|
$log = BotLog::find($logId);
|
||||||
|
if ($log) {
|
||||||
|
LivewireAlert::title('Error Details')
|
||||||
|
->html("<pre class='whitespace-pre-wrap break-words'>{$log->error}</pre>")
|
||||||
|
->warning()
|
||||||
|
->timer(null)
|
||||||
|
->withCancelButton('Close')
|
||||||
|
->show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showOutput(int $logId): void
|
||||||
|
{
|
||||||
|
$log = BotLog::find($logId);
|
||||||
|
if ($log) {
|
||||||
|
$output = $log->output ?? 'No output';
|
||||||
|
LivewireAlert::title('Output Details')
|
||||||
|
->html("<pre class='text-left rounded p-4 bg-gray-200 whitespace-pre-wrap break-words'><code>{$output}</code</pre>")
|
||||||
|
->info()
|
||||||
|
->timer(null)
|
||||||
|
->withCancelButton('Close')
|
||||||
|
->show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Jobs\RunBot;
|
||||||
use App\Models\Bot;
|
use App\Models\Bot;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Jantinnerezo\LivewireAlert\Facades\LivewireAlert;
|
use Jantinnerezo\LivewireAlert\Facades\LivewireAlert;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -10,9 +12,19 @@ class BotsList extends Component
|
|||||||
{
|
{
|
||||||
public $bots;
|
public $bots;
|
||||||
|
|
||||||
|
public string $query = '';
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->bots = \App\Models\Bot::all();
|
$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)
|
public function toggleBot($botId)
|
||||||
@@ -60,10 +72,13 @@ class BotsList extends Component
|
|||||||
|
|
||||||
public function confirmRunBot(Bot $bot): void
|
public function confirmRunBot(Bot $bot): void
|
||||||
{
|
{
|
||||||
// Dispatch the job to run the bot
|
$log = $bot->logs()->create([
|
||||||
$class = new $bot->class($bot->config ?? []);
|
'status' => 'pending',
|
||||||
|
'started_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
$class->run();
|
// Dispatch the job to run the bot
|
||||||
|
dispatch(new RunBot($bot->id, $log->id));
|
||||||
|
|
||||||
flash()->success("Bot '{$bot->name}' is being executed.");
|
flash()->success("Bot '{$bot->name}' is being executed.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Bots\BotContract;
|
use App\Interfaces\BaseContract;
|
||||||
use App\Models\Bot;
|
use App\Models\Bot;
|
||||||
use Cron\CronExpression;
|
use Cron\CronExpression;
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@@ -59,7 +58,7 @@ class CreateEditBot extends Component
|
|||||||
$class = $baseNamespace . '\\' . $relativeNamespace;
|
$class = $baseNamespace . '\\' . $relativeNamespace;
|
||||||
|
|
||||||
// Make sure class exists and implements BotContract
|
// Make sure class exists and implements BotContract
|
||||||
if (class_exists($class) && in_array(BotContract::class, class_implements($class))) {
|
if (class_exists($class) && in_array(BaseContract::class, class_implements($class))) {
|
||||||
$label = method_exists($class, 'label')
|
$label = method_exists($class, 'label')
|
||||||
? $class::label()
|
? $class::label()
|
||||||
: class_basename($class);
|
: class_basename($class);
|
||||||
@@ -71,8 +70,7 @@ class CreateEditBot extends Component
|
|||||||
|
|
||||||
public function updatedClass($value)
|
public function updatedClass($value)
|
||||||
{
|
{
|
||||||
$this->class = $this->classList[$value];
|
$this->configSchema = $this->class::configSchema();
|
||||||
$this->configSchema = $this->classList[$value]::configSchema();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
@@ -111,6 +109,8 @@ class CreateEditBot extends Component
|
|||||||
|
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
|
$this->schedule = trim(preg_replace('/\s{2,}/', ' ', $this->schedule));
|
||||||
|
$this->updatedSchedule($this->schedule);
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
if (!class_exists($this->class)) {
|
if (!class_exists($this->class)) {
|
||||||
@@ -118,7 +118,7 @@ class CreateEditBot extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array(\App\Bots\BotContract::class, class_implements($this->class))) {
|
if (!in_array(BaseContract::class, class_implements($this->class))) {
|
||||||
$this->addError('class', 'The specified class does not exist.');
|
$this->addError('class', 'The specified class does not exist.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,6 +142,7 @@ class CreateEditBot extends Component
|
|||||||
'enabled' => $this->enabled,
|
'enabled' => $this->enabled,
|
||||||
'schedule' => $this->schedule,
|
'schedule' => $this->schedule,
|
||||||
'config' => $this->config,
|
'config' => $this->config,
|
||||||
|
'user_id' => auth()->user()->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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->id, $log->id));
|
||||||
|
|
||||||
|
flash()->success("Bot '{$this->bot->name}' is being executed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.view-bot');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use Cron\CronExpression;
|
use Cron\CronExpression;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Lorisleiva\CronTranslator\CronTranslator;
|
use Lorisleiva\CronTranslator\CronTranslator;
|
||||||
|
|
||||||
class Bot extends Model
|
class Bot extends Model
|
||||||
@@ -14,7 +15,8 @@ class Bot extends Model
|
|||||||
'class',
|
'class',
|
||||||
'config',
|
'config',
|
||||||
'schedule',
|
'schedule',
|
||||||
'enabled'
|
'enabled',
|
||||||
|
'user_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -44,4 +46,24 @@ class Bot extends Model
|
|||||||
get: fn() => class_basename($this->class)
|
get: fn() => class_basename($this->class)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
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\Models;
|
|||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -58,4 +59,9 @@ class User extends Authenticatable
|
|||||||
->map(fn ($word) => Str::substr($word, 0, 1))
|
->map(fn ($word) => Str::substr($word, 0, 1))
|
||||||
->implode('');
|
->implode('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function bots(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Bot::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/Policies/BotPolicy.php
Normal file
23
app/Policies/BotPolicy.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Bot;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class BotPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new policy instance.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
return Auth::check();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $user, Bot $bot): bool
|
||||||
|
{
|
||||||
|
return $user->id === $bot->user_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Interfaces\BaseContract;
|
||||||
use App\Models\Bot;
|
use App\Models\Bot;
|
||||||
use App\Bots\BotContract;
|
use App\Interfaces\BotContract;
|
||||||
|
use App\Interfaces\ScriptContract;
|
||||||
|
use App\Jobs\RunBot;
|
||||||
use Cron\CronExpression;
|
use Cron\CronExpression;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@@ -16,14 +19,29 @@ class BotService
|
|||||||
foreach ($bots as $bot) {
|
foreach ($bots as $bot) {
|
||||||
$cron = new CronExpression($bot->schedule);
|
$cron = new CronExpression($bot->schedule);
|
||||||
if ($cron->isDue()) {
|
if ($cron->isDue()) {
|
||||||
|
$log = $bot->logs()->create([
|
||||||
|
'status' => 'pending',
|
||||||
|
'started_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$instance = app($bot->class, ['config' => $bot->config]);
|
$instance = app($bot->class, ['config' => $bot->config]);
|
||||||
|
|
||||||
if ($instance instanceof BotContract) {
|
if ($instance instanceof BaseContract) {
|
||||||
$instance->run();
|
RunBot::dispatch(bot_id: $bot->id, log_id: $log->id);
|
||||||
|
$log->update([
|
||||||
|
'started_at' => now(),
|
||||||
|
'status' => 'pending'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::error("Bot [{$bot->name}] failed: " . $e->getMessage());
|
Log::error("Bot [{$bot->name}] failed: " . $e->getMessage(), ['exception' => $e]);
|
||||||
|
|
||||||
|
$log->update([
|
||||||
|
'finished_at' => now(),
|
||||||
|
'status' => 'failed',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::info("Bot [{$bot->name}] executed successfully.");
|
Log::info("Bot [{$bot->name}] executed successfully.");
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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::table('bots', function (Blueprint $table) {
|
||||||
|
$table->foreignId('user_id')->references('id')->on('users')->after('id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('bots', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
image: git.oskarmikael.com/oskarb/scheduler:latest
|
|
||||||
volumes:
|
|
||||||
- storage:/var/www/html/storage
|
|
||||||
- bootstrap_cache:/var/www/html/bootstrap/cache
|
|
||||||
- .env:/var/www/html/.env
|
|
||||||
networks:
|
|
||||||
- laravel
|
|
||||||
|
|
||||||
nginx:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
target: nginx
|
|
||||||
ports:
|
|
||||||
- '${APP_PORT:-80}:80'
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
networks:
|
|
||||||
- laravel
|
|
||||||
|
|
||||||
networks:
|
|
||||||
laravel:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
storage:
|
|
||||||
bootstrap_cache:
|
|
||||||
@@ -28,13 +28,16 @@
|
|||||||
<div class="p-4"></div>
|
<div class="p-4"></div>
|
||||||
</div>
|
</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>
|
</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">
|
<div class="p-4">
|
||||||
<h2>All Bots</h2>
|
<h2>All Bots</h2>
|
||||||
|
|||||||
52
resources/views/livewire/bot-logs.blade.php
Normal file
52
resources/views/livewire/bot-logs.blade.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<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
|
||||||
|
class="hover:bg-zinc-700"
|
||||||
|
@if ($log->status === "failed")
|
||||||
|
wire:click="showError({{ $log->id }})"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@elseif ($log->status === "success")
|
||||||
|
wire:click="showOutput({{ $log->id }})"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@endif
|
||||||
|
>
|
||||||
|
<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 }}
|
||||||
|
@if ($log->status === "failed")
|
||||||
|
<i class="fas fa-exclamation-circle ml-1"></i>
|
||||||
|
@endif
|
||||||
|
</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>
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="border px-4 py-2">Name</th>
|
<th class="px-4 py-2">Name</th>
|
||||||
<th class="border px-4 py-2">Schema</th>
|
<th class="px-4 py-2">Schema</th>
|
||||||
<th class="border px-4 py-2">Schedule</th>
|
<th class="px-4 py-2">Schedule</th>
|
||||||
<th class="border px-4 py-2">Next due</th>
|
<th class="px-4 py-2">Next due</th>
|
||||||
<th class="border px-4 py-2">Enabled</th>
|
<th class="px-4 py-2">Enabled</th>
|
||||||
<th class="border px-4 py-2">Actions</th>
|
<th class="px-4 py-2">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-center">
|
<tbody class="text-center">
|
||||||
@@ -16,7 +30,7 @@
|
|||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
<a
|
<a
|
||||||
wire:navigate
|
wire:navigate
|
||||||
href="{{ route('bots.edit', $bot) }}"
|
href="{{ route('bots.show', $bot) }}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
{{ $bot->name }}
|
{{ $bot->name }}
|
||||||
@@ -37,7 +51,7 @@
|
|||||||
wire:click="toggleBot({{ $bot->id }})"
|
wire:click="toggleBot({{ $bot->id }})"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="text-xs">
|
||||||
<button
|
<button
|
||||||
wire:click="runBot({{ $bot->id }})"
|
wire:click="runBot({{ $bot->id }})"
|
||||||
class="ml-4 cursor-pointer"
|
class="ml-4 cursor-pointer"
|
||||||
|
|||||||
@@ -51,13 +51,8 @@
|
|||||||
|
|
||||||
@if ($meta['type'] === 'json')
|
@if ($meta['type'] === 'json')
|
||||||
<div x-data="jsonEditor(@entangle('config.' . $field))">
|
<div x-data="jsonEditor(@entangle('config.' . $field))">
|
||||||
<label
|
|
||||||
for="config-body"
|
|
||||||
class="block font-semibold mb-1"
|
|
||||||
>
|
|
||||||
Request Body (JSON)
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
|
wire:ignore
|
||||||
id="config-body"
|
id="config-body"
|
||||||
x-model="value"
|
x-model="value"
|
||||||
wire:model.live="config.{{ $field }}"
|
wire:model.live="config.{{ $field }}"
|
||||||
@@ -71,10 +66,20 @@
|
|||||||
>
|
>
|
||||||
Prettify
|
Prettify
|
||||||
</span>
|
</span>
|
||||||
|
@elseif ($meta['type'] === 'textarea')
|
||||||
|
<textarea
|
||||||
|
wire:ignore
|
||||||
|
wire:model.live="config.{{ $field }}"
|
||||||
|
class="border rounded w-full"
|
||||||
|
name="config[{{ $field }}]"
|
||||||
|
rows="4"
|
||||||
|
>
|
||||||
|
{{ old("config.$field", $bot->config[$field] ?? '') }}
|
||||||
|
</textarea>
|
||||||
@else
|
@else
|
||||||
<input
|
<input
|
||||||
wire:model.live="config.{{ $field }}"
|
wire:model.live="config.{{ $field }}"
|
||||||
class="border p-2 rounded mb-2 w-full"
|
class="border p-2 rounded w-full"
|
||||||
type="text"
|
type="text"
|
||||||
name="config[{{ $field }}]"
|
name="config[{{ $field }}]"
|
||||||
value="{{ old("config.$field", $bot->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>
|
||||||
@@ -9,5 +9,5 @@ Artisan::command('inspire', function () {
|
|||||||
$this->comment(Inspiring::quote());
|
$this->comment(Inspiring::quote());
|
||||||
})->purpose('Display an inspiring quote');
|
})->purpose('Display an inspiring quote');
|
||||||
|
|
||||||
Schedule::call(fn() => app(BotService::class)->run())->everyMinute();
|
Schedule::call(fn() => app(BotService::class)->run())->everyMinute()->timezone('Europe/Copenhagen');
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Livewire\EditBot;
|
|||||||
use App\Livewire\Settings\Appearance;
|
use App\Livewire\Settings\Appearance;
|
||||||
use App\Livewire\Settings\Password;
|
use App\Livewire\Settings\Password;
|
||||||
use App\Livewire\Settings\Profile;
|
use App\Livewire\Settings\Profile;
|
||||||
|
use App\Livewire\ViewBot;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
@@ -19,8 +20,10 @@ Route::get('dashboard', [App\Http\Controllers\DashboardController::class, 'index
|
|||||||
Route::middleware(['auth'])->group(function () {
|
Route::middleware(['auth'])->group(function () {
|
||||||
Route::get('bots/create', CreateEditBot::class)
|
Route::get('bots/create', CreateEditBot::class)
|
||||||
->name('bots.create');
|
->name('bots.create');
|
||||||
|
Route::get('bots/{bot}', ViewBot::class)
|
||||||
|
->name('bots.show')->middleware('can:view,bot');
|
||||||
Route::get('bots/edit/{bot}', CreateEditBot::class)
|
Route::get('bots/edit/{bot}', CreateEditBot::class)
|
||||||
->name('bots.edit');
|
->name('bots.edit')->middleware('can:view,bot');
|
||||||
|
|
||||||
Route::redirect('settings', 'settings/profile');
|
Route::redirect('settings', 'settings/profile');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user