Compare commits
2 Commits
master
...
oskarb-pat
| Author | SHA1 | Date | |
|---|---|---|---|
| 808b701596 | |||
| b62549ad90 |
@@ -6,27 +6,27 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# - name: Log in to Gitea Registry
|
||||
# run: |
|
||||
# echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.oskarmikael.com -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||||
- name: Log in to Gitea Registry
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.oskarmikael.com -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||||
|
||||
# - name: Build and Push Docker image
|
||||
# run: |
|
||||
# IMAGE=git.oskarmikael.com/oskarb/scheduler:latest
|
||||
# docker buildx build \
|
||||
# -f Dockerfile.prod \
|
||||
# --platform linux/amd64 \
|
||||
# -t $IMAGE \
|
||||
# --push .
|
||||
- name: Build and Push Docker image
|
||||
run: |
|
||||
IMAGE=git.oskarmikael.com/oskarb/scheduler:latest
|
||||
docker buildx build \
|
||||
-f Dockerfile.prod \
|
||||
--platform linux/amd64 \
|
||||
-t $IMAGE \
|
||||
--push .
|
||||
|
||||
# - name: Run migrations
|
||||
# run: |
|
||||
@@ -52,11 +52,8 @@ jobs:
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
port: 22
|
||||
script: |
|
||||
systemctl restart laravel-worker
|
||||
cd /var/www/scheduler
|
||||
git pull origin master
|
||||
npm install
|
||||
npm run build
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
php artisan optimize
|
||||
php artisan migrate --force
|
||||
/usr/lib/docker/cli-plugins/docker-compose -f docker-compose.prod.yml pull
|
||||
/usr/lib/docker/cli-plugins/docker-compose -f docker-compose.prod.yml up -d
|
||||
/usr/lib/docker/cli-plugins/docker-compose -f docker-compose.prod.yml run --rm app php artisan migrate --force
|
||||
|
||||
70
Dockerfile.prod
Normal file
70
Dockerfile.prod
Normal file
@@ -0,0 +1,70 @@
|
||||
# ----------------------------
|
||||
# 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 ./docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
CMD ["php-fpm"]
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
9
app/Bots/BotContract.php
Normal file
9
app/Bots/BotContract.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bots;
|
||||
|
||||
interface BotContract
|
||||
{
|
||||
public function run(): void;
|
||||
public static function configSchema(): array;
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Bots;
|
||||
|
||||
use App\Interfaces\BotContract;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GenericApi implements BotContract
|
||||
{
|
||||
@@ -17,8 +17,9 @@ class GenericApi implements BotContract
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function run(): JsonResponse
|
||||
public function run(): void
|
||||
{
|
||||
try {
|
||||
$this->client = new Client();
|
||||
|
||||
$options = [];
|
||||
@@ -31,16 +32,14 @@ class GenericApi implements BotContract
|
||||
$options['json'] = json_decode($this->config['body'], true);
|
||||
}
|
||||
|
||||
$response = $this->client->request(
|
||||
$this->client->request(
|
||||
$this->config['method'] ?? 'GET',
|
||||
$this->config['url'],
|
||||
$options
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => $response->getStatusCode(),
|
||||
'body' => json_decode((string) $response->getBody(), true),
|
||||
]);
|
||||
} catch (GuzzleException $e) {
|
||||
Log::error("Call to {$this->config['url']} failed" . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function configSchema(): array
|
||||
|
||||
75
app/Bots/Mattermost.php
Normal file
75
app/Bots/Mattermost.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?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,13 +3,12 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$bots = Auth::user()->bots;
|
||||
$bots = \App\Models\Bot::all();
|
||||
|
||||
return view('dashboard', compact('bots'));
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Interfaces;
|
||||
|
||||
interface BaseContract
|
||||
{
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Interfaces;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
interface BotContract extends BaseContract
|
||||
{
|
||||
public function run(): JsonResponse;
|
||||
public static function configSchema(): array;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Interfaces;
|
||||
|
||||
interface ScriptContract extends BaseContract
|
||||
{
|
||||
public function run(): string;
|
||||
public static function configSchema(): array;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?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,9 +2,7 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\RunBot;
|
||||
use App\Models\Bot;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Jantinnerezo\LivewireAlert\Facades\LivewireAlert;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -12,19 +10,9 @@ 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();
|
||||
$this->bots = \App\Models\Bot::all();
|
||||
}
|
||||
|
||||
public function toggleBot($botId)
|
||||
@@ -72,13 +60,10 @@ 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->id, $log->id));
|
||||
$class = new $bot->class($bot->config ?? []);
|
||||
|
||||
$class->run();
|
||||
|
||||
flash()->success("Bot '{$bot->name}' is being executed.");
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Interfaces\BaseContract;
|
||||
use App\Bots\BotContract;
|
||||
use App\Models\Bot;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -58,7 +59,7 @@ class CreateEditBot extends Component
|
||||
$class = $baseNamespace . '\\' . $relativeNamespace;
|
||||
|
||||
// Make sure class exists and implements BotContract
|
||||
if (class_exists($class) && in_array(BaseContract::class, class_implements($class))) {
|
||||
if (class_exists($class) && in_array(BotContract::class, class_implements($class))) {
|
||||
$label = method_exists($class, 'label')
|
||||
? $class::label()
|
||||
: class_basename($class);
|
||||
@@ -70,7 +71,8 @@ class CreateEditBot extends Component
|
||||
|
||||
public function updatedClass($value)
|
||||
{
|
||||
$this->configSchema = $this->class::configSchema();
|
||||
$this->class = $this->classList[$value];
|
||||
$this->configSchema = $this->classList[$value]::configSchema();
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
@@ -109,8 +111,6 @@ class CreateEditBot extends Component
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->schedule = trim(preg_replace('/\s{2,}/', ' ', $this->schedule));
|
||||
$this->updatedSchedule($this->schedule);
|
||||
$this->validate();
|
||||
|
||||
if (!class_exists($this->class)) {
|
||||
@@ -118,7 +118,7 @@ class CreateEditBot extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array(BaseContract::class, class_implements($this->class))) {
|
||||
if (!in_array(\App\Bots\BotContract::class, class_implements($this->class))) {
|
||||
$this->addError('class', 'The specified class does not exist.');
|
||||
return;
|
||||
}
|
||||
@@ -142,7 +142,6 @@ class CreateEditBot extends Component
|
||||
'enabled' => $this->enabled,
|
||||
'schedule' => $this->schedule,
|
||||
'config' => $this->config,
|
||||
'user_id' => auth()->user()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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,7 +5,6 @@ namespace App\Models;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Lorisleiva\CronTranslator\CronTranslator;
|
||||
|
||||
class Bot extends Model
|
||||
@@ -15,8 +14,7 @@ class Bot extends Model
|
||||
'class',
|
||||
'config',
|
||||
'schedule',
|
||||
'enabled',
|
||||
'user_id',
|
||||
'enabled'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -46,24 +44,4 @@ class Bot extends Model
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?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,7 +4,6 @@ namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -59,9 +58,4 @@ class User extends Authenticatable
|
||||
->map(fn ($word) => Str::substr($word, 0, 1))
|
||||
->implode('');
|
||||
}
|
||||
|
||||
public function bots(): HasMany
|
||||
{
|
||||
return $this->hasMany(Bot::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?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,11 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Interfaces\BaseContract;
|
||||
use App\Models\Bot;
|
||||
use App\Interfaces\BotContract;
|
||||
use App\Interfaces\ScriptContract;
|
||||
use App\Jobs\RunBot;
|
||||
use App\Bots\BotContract;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -19,29 +16,14 @@ 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 BaseContract) {
|
||||
RunBot::dispatch(bot_id: $bot->id, log_id: $log->id);
|
||||
$log->update([
|
||||
'started_at' => now(),
|
||||
'status' => 'pending'
|
||||
]);
|
||||
if ($instance instanceof BotContract) {
|
||||
$instance->run();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Bot [{$bot->name}] failed: " . $e->getMessage(), ['exception' => $e]);
|
||||
|
||||
$log->update([
|
||||
'finished_at' => now(),
|
||||
'status' => 'failed',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
Log::error("Bot [{$bot->name}] failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
Log::info("Bot [{$bot->name}] executed successfully.");
|
||||
|
||||
@@ -65,7 +65,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
<?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
docker-compose.prod.yml
Normal file
28
docker-compose.prod.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
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:
|
||||
21
docker/nginx/nginx.conf
Normal file
21
docker/nginx/nginx.conf
Normal file
@@ -0,0 +1,21 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name scheduler.oskarmikael.com;
|
||||
|
||||
root /var/www/html/public;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass app:9000;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,13 @@
|
||||
<div class="p-4"></div>
|
||||
</div>
|
||||
<div
|
||||
class="relative aspect-video overflow-scroll rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
>
|
||||
<div class="p-4">Logs</div>
|
||||
<div class="">
|
||||
@livewire('bot-logs')
|
||||
</div>
|
||||
<div class="p-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative h-full flex-1 overflow-scroll rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700"
|
||||
>
|
||||
<div class="p-4">
|
||||
<h2>All Bots</h2>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<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,27 +1,13 @@
|
||||
<div>
|
||||
<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>
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-center">
|
||||
@@ -30,7 +16,7 @@
|
||||
<td class="px-4 py-2">
|
||||
<a
|
||||
wire:navigate
|
||||
href="{{ route('bots.show', $bot) }}"
|
||||
href="{{ route('bots.edit', $bot) }}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
{{ $bot->name }}
|
||||
@@ -51,7 +37,7 @@
|
||||
wire:click="toggleBot({{ $bot->id }})"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<td>
|
||||
<button
|
||||
wire:click="runBot({{ $bot->id }})"
|
||||
class="ml-4 cursor-pointer"
|
||||
|
||||
@@ -51,8 +51,13 @@
|
||||
|
||||
@if ($meta['type'] === 'json')
|
||||
<div x-data="jsonEditor(@entangle('config.' . $field))">
|
||||
<label
|
||||
for="config-body"
|
||||
class="block font-semibold mb-1"
|
||||
>
|
||||
Request Body (JSON)
|
||||
</label>
|
||||
<textarea
|
||||
wire:ignore
|
||||
id="config-body"
|
||||
x-model="value"
|
||||
wire:model.live="config.{{ $field }}"
|
||||
@@ -66,20 +71,10 @@
|
||||
>
|
||||
Prettify
|
||||
</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
|
||||
<input
|
||||
wire:model.live="config.{{ $field }}"
|
||||
class="border p-2 rounded w-full"
|
||||
class="border p-2 rounded mb-2 w-full"
|
||||
type="text"
|
||||
name="config[{{ $field }}]"
|
||||
value="{{ old("config.$field", $bot->config[$field] ?? '') }}"
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<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());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::call(fn() => app(BotService::class)->run())->everyMinute()->timezone('Europe/Copenhagen');
|
||||
Schedule::call(fn() => app(BotService::class)->run())->everyMinute();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 () {
|
||||
@@ -20,10 +19,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');
|
||||
->name('bots.edit');
|
||||
|
||||
Route::redirect('settings', 'settings/profile');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user