Compare commits

..

26 Commits
1.2.0 ... 1.4.0

Author SHA1 Message Date
e2dd9177f7 fix number format 2024-04-17 22:08:19 +02:00
5a9e8d6799 fix monitoring numbers 2024-04-17 21:09:09 +02:00
868b70f530 add cron to docker 2024-04-17 17:14:12 +02:00
d07e9bcad2 remote monitor (#167) 2024-04-17 16:03:06 +02:00
0cd815cce6 ui fix and build 2024-04-14 18:17:44 +02:00
5ab6617b5d fix read file 2024-04-14 17:53:08 +02:00
72b37c56fd ui fix 2024-04-14 14:53:58 +02:00
8a4ef66946 update Feature/add remote server logs (#166) 2024-04-14 14:41:00 +02:00
4517ca7d2a Feature/add remote server logs (#159) 2024-04-14 14:34:47 +02:00
75aed62d75 fix search bar position (#165) 2024-04-14 09:52:06 +02:00
aaef73d89d fix custom vhost update (#164) 2024-04-13 23:47:52 +02:00
f03a029e36 fix metrics page 2024-04-13 22:44:11 +02:00
52d195710b add data retention to the metrics 2024-04-13 22:38:27 +02:00
ddacc32e64 docker release action 2024-04-13 13:19:20 +02:00
2ae9a14d02 docker 2024-04-13 12:44:12 +02:00
3019c3d213 fix docker 2024-04-13 12:31:53 +02:00
c43869d255 docker 2024-04-13 12:23:28 +02:00
18748f77ac build 2024-04-13 11:50:24 +02:00
052e28d2e3 Monitoring & Service Management (#163)
Monitoring & Service Management
2024-04-13 11:47:56 +02:00
87ec0af697 update demo link 2024-04-07 20:19:56 +02:00
e9016737d4 build frontend 2024-04-05 19:49:35 +02:00
f34d5eb82b Bump vite from 4.5.2 to 4.5.3 (#152) 2024-04-05 19:48:37 +02:00
12c500e125 Bug fixes (#155) 2024-04-05 19:45:09 +02:00
2d566b853f use textarea for code editor (#151) 2024-04-03 22:38:28 +02:00
ca93b521ec Merge branch 'main' into 1.x 2024-04-01 21:19:14 +02:00
a0af4e3e9d Merge pull request #128 from vitodeploy/1.x
Merge
2024-03-24 10:07:20 +01:00
176 changed files with 4523 additions and 657 deletions

35
.github/workflows/docker-1x.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Build and push Docker image
on:
push:
branches:
- 1.x
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
run: |
docker buildx build . \
-f docker/Dockerfile \
-t vitodeploy/vito:1.x \
--build-arg="RELEASE=0" \
--platform linux/amd64,linux/arm64 \
--no-cache \
--push

35
.github/workflows/docker-release.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Build and push Docker image
on:
release:
types: [created]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
run: |
docker buildx build . \
-f docker/Dockerfile \
-t vitodeploy/vito:${{ github.event.release.tag_name }} \
-t vitodeploy/vito:latest \
--build-arg="RELEASE=0" \
--platform linux/amd64,linux/arm64 \
--no-cache \
--push

View File

@ -36,7 +36,7 @@ ## Useful Links
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
- [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Video Demo](https://youtu.be/rLRHIyEfON8)
- [Video Demo](https://youtu.be/AbmUOBDOc28)
- [Discord](https://discord.gg/uZeeHZZnm5)
- [Contribution](/CONTRIBUTING.md)
- [Security](/SECURITY.md)

View File

@ -0,0 +1,150 @@
<?php
namespace App\Actions\Monitoring;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class GetMetrics
{
public function filter(Server $server, array $input): array
{
if (isset($input['from']) && isset($input['to']) && $input['from'] === $input['to']) {
$input['from'] = Carbon::parse($input['from'])->format('Y-m-d').' 00:00:00';
$input['to'] = Carbon::parse($input['to'])->format('Y-m-d').' 23:59:59';
}
$defaultInput = [
'period' => '10m',
];
$input = array_merge($defaultInput, $input);
$this->validate($input);
return $this->metrics(
server: $server,
fromDate: $this->getFromDate($input),
toDate: $this->getToDate($input),
interval: $this->getInterval($input)
);
}
private function metrics(
Server $server,
Carbon $fromDate,
Carbon $toDate,
?Expression $interval = null
): array {
$metrics = DB::table('metrics')
->where('server_id', $server->id)
->whereBetween('created_at', [$fromDate->format('Y-m-d H:i:s'), $toDate->format('Y-m-d H:i:s')])
->select(
[
DB::raw('created_at as date'),
DB::raw('ROUND(AVG(load), 2) as load'),
DB::raw('ROUND(AVG(memory_total), 2) as memory_total'),
DB::raw('ROUND(AVG(memory_used), 2) as memory_used'),
DB::raw('ROUND(AVG(memory_free), 2) as memory_free'),
DB::raw('ROUND(AVG(disk_total), 2) as disk_total'),
DB::raw('ROUND(AVG(disk_used), 2) as disk_used'),
DB::raw('ROUND(AVG(disk_free), 2) as disk_free'),
$interval,
],
)
->groupByRaw('date_interval')
->orderBy('date_interval')
->get()
->map(function ($item) {
$item->date = Carbon::parse($item->date)->format('Y-m-d H:i');
return $item;
});
return [
'metrics' => $metrics,
];
}
private function getFromDate(array $input): Carbon
{
if ($input['period'] === 'custom') {
return new Carbon($input['from']);
}
return Carbon::parse('-'.convert_time_format($input['period']));
}
private function getToDate(array $input): Carbon
{
if ($input['period'] === 'custom') {
return new Carbon($input['to']);
}
return Carbon::now();
}
private function getInterval(array $input): Expression
{
if ($input['period'] === 'custom') {
$from = new Carbon($input['from']);
$to = new Carbon($input['to']);
$periodInHours = $from->diffInHours($to);
}
if (! isset($periodInHours)) {
$periodInHours = Carbon::parse(
convert_time_format($input['period'])
)->diffInHours();
}
if ($periodInHours <= 1) {
return DB::raw("strftime('%Y-%m-%d %H:%M:00', created_at) as date_interval");
}
if ($periodInHours <= 24) {
return DB::raw("strftime('%Y-%m-%d %H:00:00', created_at) as date_interval");
}
if ($periodInHours > 24) {
return DB::raw("strftime('%Y-%m-%d 00:00:00', created_at) as date_interval");
}
}
private function validate(array $input): void
{
Validator::make($input, [
'period' => [
'required',
Rule::in([
'10m',
'30m',
'1h',
'12h',
'1d',
'7d',
'custom',
]),
],
])->validate();
if ($input['period'] === 'custom') {
Validator::make($input, [
'from' => [
'required',
'date',
'before:to',
],
'to' => [
'required',
'date',
'after:from',
],
])->validate();
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Actions\Monitoring;
use App\Models\Server;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateMetricSettings
{
public function update(Server $server, array $input): void
{
$this->validate($input);
$service = $server->monitoring();
$data = $service->handler()->data();
$data['data_retention'] = $input['data_retention'];
$service->type_data = $data;
$service->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'data_retention' => [
'required',
Rule::in(config('core.metrics_data_retention')),
],
])->validate();
}
}

View File

@ -4,6 +4,7 @@
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Validation\ValidationException;
class ChangeDefaultCli
@ -12,7 +13,9 @@ public function change(Server $server, array $input): void
{
$this->validate($server, $input);
$service = $server->php($input['version']);
$service->handler()->setDefaultCli();
/** @var PHP $handler */
$handler = $service->handler();
$handler->setDefaultCli();
$server->defaultService('php')->update(['is_default' => 0]);
$service->update(['is_default' => 1]);
$service->update(['status' => ServiceStatus::READY]);

View File

@ -3,6 +3,7 @@
namespace App\Actions\PHP;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Validation\ValidationException;
class GetPHPIni
@ -14,7 +15,10 @@ public function getIni(Server $server, array $input): string
$php = $server->php($input['version']);
try {
return $php->handler()->getPHPIni();
/** @var PHP $handler */
$handler = $php->handler();
return $handler->getPHPIni();
} catch (\Throwable $e) {
throw ValidationException::withMessages(
['ini' => $e->getMessage()]

View File

@ -4,6 +4,7 @@
use App\Models\Server;
use App\Models\Service;
use App\SSH\Services\PHP\PHP;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -23,7 +24,9 @@ public function install(Server $server, array $input): Service
$service->save();
dispatch(function () use ($service, $input) {
$service->handler()->installExtension($input['extension']);
/** @var PHP $handler */
$handler = $service->handler();
$handler->installExtension($input['extension']);
})->catch(function () use ($service, $input) {
$service->refresh();
$typeData = $service->type_data;

View File

@ -34,8 +34,6 @@ public function create(Site $site, array $input): void
$ssl->status = SslStatus::CREATED;
$ssl->save();
$site->type()->edit();
})->catch(function () use ($ssl) {
$ssl->delete();
});
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateServerLog
{
/**
* @throws ValidationException
*/
public function create(Server $server, array $input): void
{
$this->validate($input);
$server->logs()->create([
'is_remote' => true,
'name' => $input['path'],
'type' => 'remote',
'disk' => 'ssh',
]);
}
/**
* @throws ValidationException
*/
protected function validate(array $input): void
{
Validator::make($input, [
'path' => 'required',
])->validate();
}
}

View File

@ -8,14 +8,15 @@
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class Create
class Install
{
public function create(Server $server, array $input): Service
public function install(Server $server, array $input): Service
{
$this->validate($server, $input);
$service = new Service([
'name' => $input['type'],
'server_id' => $server->id,
'name' => $input['name'],
'type' => $input['type'],
'version' => $input['version'],
'status' => ServiceStatus::INSTALLING,
@ -27,15 +28,13 @@ public function create(Server $server, array $input): Service
$service->save();
$service->handler()->create();
dispatch(function () use ($service) {
$service->handler()->install();
$service->status = ServiceStatus::READY;
$service->save();
})->catch(function () use ($service) {
$service->handler()->delete();
$service->delete();
$service->status = ServiceStatus::INSTALLATION_FAILED;
$service->save();
})->onConnection('ssh');
return $service;
@ -46,8 +45,11 @@ private function validate(Server $server, array $input): void
Validator::make($input, [
'type' => [
'required',
Rule::in(config('core.add_on_services')),
Rule::unique('services', 'type')->where('server_id', $server->id),
Rule::in(config('core.service_types')),
],
'name' => [
'required',
Rule::in(array_keys(config('core.service_types'))),
],
'version' => 'required',
])->validate();

View File

@ -0,0 +1,28 @@
<?php
namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Service;
use Illuminate\Support\Facades\Validator;
class Uninstall
{
public function uninstall(Service $service): void
{
Validator::make([
'service' => $service->id,
], $service->handler()->deletionRules())->validate();
$service->status = ServiceStatus::UNINSTALLING;
$service->save();
dispatch(function () use ($service) {
$service->handler()->uninstall();
$service->delete();
})->catch(function () use ($service) {
$service->status = ServiceStatus::FAILED;
$service->save();
})->onConnection('ssh');
}
}

View File

@ -3,6 +3,8 @@
namespace App\Actions\Site;
use App\Enums\SiteStatus;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Models\Server;
@ -19,7 +21,6 @@
class CreateSite
{
/**
* @throws SourceControlIsNotConnected
* @throws ValidationException
*/
public function create(Server $server, array $input): Site
@ -47,7 +48,15 @@ public function create(Server $server, array $input): Site
}
} catch (SourceControlIsNotConnected) {
throw ValidationException::withMessages([
'source_control' => __('Source control is not connected'),
'source_control' => 'Source control is not connected',
]);
} catch (RepositoryPermissionDenied) {
throw ValidationException::withMessages([
'repository' => 'You do not have permission to access this repository',
]);
} catch (RepositoryNotFound) {
throw ValidationException::withMessages([
'repository' => 'Repository not found',
]);
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Actions\Site;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class UpdateSourceControl
{
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->source_control_id = $input['source_control'];
try {
if ($site->sourceControl()) {
$site->sourceControl()->getRepo($site->repository);
}
} catch (SourceControlIsNotConnected) {
throw ValidationException::withMessages([
'source_control' => 'Source control is not connected',
]);
} catch (RepositoryPermissionDenied) {
throw ValidationException::withMessages([
'repository' => 'You do not have permission to access this repository',
]);
} catch (RepositoryNotFound) {
throw ValidationException::withMessages([
'repository' => 'Repository not found',
]);
}
$site->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'source_control' => [
'required',
Rule::exists('source_controls', 'id'),
],
])->validate();
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Console\Commands;
use App\Models\Service;
use Illuminate\Console\Command;
class DeleteOlderMetricsCommand extends Command
{
protected $signature = 'metrics:delete-older-metrics';
protected $description = 'Delete older metrics from database';
public function handle()
{
Service::query()->where('type', 'monitoring')->chunk(100, function ($services) {
$services->each(function ($service) {
$this->info("Deleting older metrics for service {$service->server->name}");
$service
->server
->metrics()
->where('created_at', '<', now()->subDays($service->handler()->data()['data_retention']))
->delete();
$this->info('Metrics deleted');
});
});
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
class GetMetricsCommand extends Command
{
protected $signature = 'metrics:get';
protected $description = 'Get server metrics';
public function handle(): void
{
$checkedMetrics = 0;
Server::query()->whereHas('services', function (Builder $query) {
$query->where('type', 'monitoring')
->where('name', 'remote-monitor');
})->chunk(10, function ($servers) use (&$checkedMetrics) {
/** @var Server $server */
foreach ($servers as $server) {
$info = $server->os()->resourceInfo();
$server->metrics()->create(array_merge($info, ['server_id' => $server->id]));
$checkedMetrics++;
}
});
$this->info("Checked $checkedMetrics metrics");
}
}

View File

@ -16,6 +16,8 @@ protected function schedule(Schedule $schedule): void
$schedule->command('backups:run "0 0 * * *"')->daily();
$schedule->command('backups:run "0 0 * * 0"')->weekly();
$schedule->command('backups:run "0 0 1 * *"')->monthly();
$schedule->command('metrics:delete-older-metrics')->daily();
$schedule->command('metrics:get')->everyMinute();
}
/**

View File

@ -2,9 +2,7 @@
namespace App\Exceptions;
use Exception;
class SSHAuthenticationError extends Exception
class SSHAuthenticationError extends SSHError
{
//
}

View File

@ -2,9 +2,7 @@
namespace App\Exceptions;
use Exception;
class SSLCreationException extends Exception
class SSLCreationException extends SSHError
{
//
}

View File

@ -133,7 +133,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
$this->log?->write($output);
if (Str::contains($output, 'VITO_SSH_ERROR')) {
throw new Exception('SSH command failed with an error');
throw new SSHCommandError('SSH command failed with an error');
}
return $output;

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AgentController extends Controller
{
public function __invoke(Request $request, Server $server, int $id): JsonResponse
{
$validated = $this->validate($request, [
'load' => 'required|numeric',
'memory_total' => 'required|numeric',
'memory_used' => 'required|numeric',
'memory_free' => 'required|numeric',
'disk_total' => 'required|numeric',
'disk_used' => 'required|numeric',
'disk_free' => 'required|numeric',
]);
/** @var Service $service */
$service = $server->services()->findOrFail($id);
if ($request->header('secret') !== $service->handler()->data()['secret']) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$server->metrics()->create(array_merge($validated, ['server_id' => $server->id]));
return response()->json();
}
}

View File

@ -7,6 +7,8 @@
use App\Actions\Site\UpdateDeploymentScript;
use App\Actions\Site\UpdateEnv;
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
@ -24,12 +26,14 @@ public function deploy(Server $server, Site $site): HtmxResponse
app(Deploy::class)->run($site);
Toast::success('Deployment started!');
} catch (SourceControlIsNotConnected $e) {
Toast::error($e->getMessage());
return htmx()->redirect(route('source-controls'));
} catch (SourceControlIsNotConnected) {
Toast::error('Source control is not connected. Check site\'s settings.');
} catch (DeploymentScriptIsEmptyException) {
Toast::error('Deployment script is empty!');
} catch (RepositoryPermissionDenied) {
Toast::error('You do not have permission to access this repository!');
} catch (RepositoryNotFound) {
Toast::error('Repository not found!');
}
return htmx()->back();
@ -83,6 +87,12 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
Toast::success('Auto deployment has been enabled.');
} catch (SourceControlIsNotConnected) {
Toast::error('Source control is not connected. Check site\'s settings.');
} catch (DeploymentScriptIsEmptyException) {
Toast::error('Deployment script is empty!');
} catch (RepositoryPermissionDenied) {
Toast::error('You do not have permission to access this repository!');
} catch (RepositoryNotFound) {
Toast::error('Repository not found!');
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Monitoring\GetMetrics;
use App\Actions\Monitoring\UpdateMetricSettings;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class MetricController extends Controller
{
public function index(Server $server, Request $request): View|RedirectResponse
{
$this->checkIfMonitoringServiceInstalled($server);
return view('metrics.index', [
'server' => $server,
'data' => app(GetMetrics::class)->filter($server, $request->input()),
'lastMetric' => $server->metrics()->latest()->first(),
]);
}
public function settings(Server $server, Request $request): HtmxResponse
{
$this->checkIfMonitoringServiceInstalled($server);
app(UpdateMetricSettings::class)->update($server, $request->input());
Toast::success('Metric settings updated successfully');
return htmx()->back();
}
private function checkIfMonitoringServiceInstalled(Server $server): void
{
if (! $server->monitoring()) {
abort(404, 'Monitoring service is not installed on this server');
}
}
}

View File

@ -54,7 +54,6 @@ public function show(Server $server): View
{
return view('servers.show', [
'server' => $server,
'logs' => $server->logs()->latest()->limit(10)->get(),
]);
}

View File

@ -2,10 +2,13 @@
namespace App\Http\Controllers;
use App\Actions\Server\CreateServerLog;
use App\Facades\Toast;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ServerLogController extends Controller
{
@ -13,6 +16,7 @@ public function index(Server $server): View
{
return view('server-logs.index', [
'server' => $server,
'pageTitle' => __('Vito Logs'),
]);
}
@ -26,4 +30,31 @@ public function show(Server $server, ServerLog $serverLog): RedirectResponse
'content' => $serverLog->getContent(),
]);
}
public function remote(Server $server): View
{
return view('server-logs.remote-logs', [
'server' => $server,
'remote' => true,
'pageTitle' => __('Remote Logs'),
]);
}
public function store(Server $server, Request $request): \App\Helpers\HtmxResponse
{
app(CreateServerLog::class)->create($server, $request->input());
Toast::success('Log added successfully.');
return htmx()->redirect(route('servers.logs.remote', ['server' => $server]));
}
public function destroy(Server $server, ServerLog $serverLog): RedirectResponse
{
$serverLog->delete();
Toast::success('Remote log deleted successfully.');
return redirect()->route('servers.logs.remote', ['server' => $server]);
}
}

View File

@ -2,7 +2,8 @@
namespace App\Http\Controllers;
use App\Actions\Service\Create;
use App\Actions\Service\Install;
use App\Actions\Service\Uninstall;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
@ -68,7 +69,16 @@ public function disable(Server $server, Service $service): RedirectResponse
public function install(Server $server, Request $request): HtmxResponse
{
app(Create::class)->create($server, $request->input());
app(Install::class)->install($server, $request->input());
Toast::success('Service is being uninstalled!');
return htmx()->back();
}
public function uninstall(Server $server, Service $service): HtmxResponse
{
app(Uninstall::class)->uninstall($service);
Toast::success('Service is being uninstalled!');

View File

@ -4,6 +4,7 @@
use App\Actions\Site\CreateSite;
use App\Actions\Site\DeleteSite;
use App\Enums\SiteStatus;
use App\Enums\SiteType;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
@ -42,14 +43,38 @@ public function create(Server $server): View
]);
}
public function show(Server $server, Site $site): View
public function show(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{
if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.installing', [$server, $site]));
}
return redirect()->route('servers.sites.installing', [$server, $site]);
}
return view('sites.show', [
'server' => $server,
'site' => $site,
]);
}
public function installing(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{
if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.show', [$server, $site]));
}
return redirect()->route('servers.sites.show', [$server, $site]);
}
return view('sites.installing', [
'server' => $server,
'site' => $site,
]);
}
public function destroy(Server $server, Site $site): RedirectResponse
{
app(DeleteSite::class)->delete($site);

View File

@ -13,6 +13,7 @@ public function index(Server $server, Site $site): View
return view('site-logs.index', [
'server' => $server,
'site' => $site,
'pageTitle' => __('Vito Logs'),
]);
}
}

View File

@ -2,10 +2,12 @@
namespace App\Http\Controllers;
use App\Actions\Site\UpdateSourceControl;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
use App\Models\Site;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@ -24,7 +26,10 @@ public function index(Server $server, Site $site): View
public function getVhost(Server $server, Site $site): RedirectResponse
{
return back()->with('vhost', $server->webserver()->handler()->getVHost($site));
/** @var Webserver $handler */
$handler = $server->webserver()->handler();
return back()->with('vhost', $handler->getVHost($site));
}
public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse
@ -34,7 +39,9 @@ public function updateVhost(Server $server, Site $site, Request $request): Redir
]);
try {
$server->webserver()->handler()->updateVHost($site, false, $request->input('vhost'));
/** @var Webserver $handler */
$handler = $server->webserver()->handler();
$handler->updateVHost($site, false, $request->input('vhost'));
Toast::success('VHost updated successfully!');
} catch (Throwable $e) {
@ -63,4 +70,13 @@ public function updatePHPVersion(Server $server, Site $site, Request $request):
return htmx()->back();
}
public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse
{
$site = app(UpdateSourceControl::class)->update($site, $request->input());
Toast::success('Source control updated successfully!');
return htmx()->back();
}
}

53
app/Models/Metric.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $server_id
* @property float $load
* @property float $memory_total
* @property float $memory_used
* @property float $memory_free
* @property float $disk_total
* @property float $disk_used
* @property float $disk_free
* @property Server $server
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class Metric extends Model
{
use HasFactory;
protected $fillable = [
'server_id',
'load',
'memory_total',
'memory_used',
'memory_free',
'disk_total',
'disk_used',
'disk_free',
];
protected $casts = [
'server_id' => 'integer',
'load' => 'float',
'memory_total' => 'float',
'memory_used' => 'float',
'memory_free' => 'float',
'disk_total' => 'float',
'disk_used' => 'float',
'disk_free' => 'float',
];
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
}

View File

@ -195,6 +195,11 @@ public function daemons(): HasMany
return $this->queues()->whereNull('site_id');
}
public function metrics(): HasMany
{
return $this->hasMany(Metric::class);
}
public function sshKeys(): BelongsToMany
{
return $this->belongsToMany(SshKey::class, 'server_ssh_keys')
@ -325,6 +330,24 @@ public function php(?string $version = null): ?Service
return $this->service('php', $version);
}
public function memoryDatabase(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('memory_database');
}
return $this->service('memory_database', $version);
}
public function monitoring(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('monitoring');
}
return $this->service('monitoring', $version);
}
public function sshKey(): array
{
/** @var FilesystemAdapter $storageDisk */

View File

@ -16,6 +16,7 @@
* @property string $disk
* @property Server $server
* @property ?Site $site
* @property bool $is_remote
*/
class ServerLog extends AbstractModel
{
@ -27,11 +28,13 @@ class ServerLog extends AbstractModel
'type',
'name',
'disk',
'is_remote',
];
protected $casts = [
'server_id' => 'integer',
'site_id' => 'integer',
'is_remote' => 'boolean',
];
public static function boot(): void
@ -64,6 +67,17 @@ public function site(): BelongsTo
return $this->belongsTo(Site::class);
}
public static function getRemote($query, bool $active = true, ?Site $site = null)
{
$query->where('is_remote', $active);
if ($site) {
$query->where('name', 'like', $site->path.'%');
}
return $query;
}
public function write($buf): void
{
if (Str::contains($buf, 'VITO_SSH_ERROR')) {
@ -78,6 +92,10 @@ public function write($buf): void
public function getContent(): ?string
{
if ($this->is_remote) {
return $this->server->os()->tail($this->name, 150);
}
if (Storage::disk($this->disk)->exists($this->name)) {
return Storage::disk($this->disk)->get($this->name);
}

View File

@ -4,13 +4,7 @@
use App\Actions\Service\Manage;
use App\Exceptions\ServiceInstallationFailed;
use App\SSH\Services\AddOnServices\AbstractAddOnService;
use App\SSH\Services\Database\Database as DatabaseHandler;
use App\SSH\Services\Firewall\Firewall as FirewallHandler;
use App\SSH\Services\PHP\PHP as PHPHandler;
use App\SSH\Services\ProcessManager\ProcessManager as ProcessManagerHandler;
use App\SSH\Services\Redis\Redis as RedisHandler;
use App\SSH\Services\Webserver\Webserver as WebserverHandler;
use App\SSH\Services\ServiceInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
@ -65,8 +59,8 @@ public function server(): BelongsTo
return $this->belongsTo(Server::class);
}
public function handler(
): PHPHandler|WebserverHandler|DatabaseHandler|FirewallHandler|ProcessManagerHandler|RedisHandler|AbstractAddOnService {
public function handler(): ServiceInterface
{
$handler = config('core.service_handlers')[$this->name];
return new $handler($this);

View File

@ -4,6 +4,7 @@
use App\Exceptions\SourceControlIsNotConnected;
use App\SiteTypes\SiteType;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -185,7 +186,9 @@ public function php(): ?Service
public function changePHPVersion($version): void
{
$this->server->webserver()->handler()->changePHPVersion($this, $version);
/** @var Webserver $handler */
$handler = $this->server->webserver()->handler();
$handler->changePHPVersion($this, $version);
$this->php_version = $version;
$this->save();
}

View File

@ -117,6 +117,16 @@ public function readFile(string $path): string
);
}
public function tail(string $path, int $lines): string
{
return $this->server->ssh()->exec(
$this->getScript('tail.sh', [
'path' => $path,
'lines' => $lines,
])
);
}
public function runScript(string $path, string $script, ?int $siteId = null): ServerLog
{
$ssh = $this->server->ssh();
@ -148,4 +158,29 @@ public function unzip(string $path): string
'unzip '.$path
);
}
public function cleanup(): void
{
$this->server->ssh()->exec(
$this->getScript('cleanup.sh'),
'cleanup'
);
}
public function resourceInfo(): array
{
$info = $this->server->ssh()->exec(
$this->getScript('resource-info.sh'),
);
return [
'load' => str($info)->after('load:')->before(PHP_EOL)->toString(),
'memory_total' => str($info)->after('memory_total:')->before(PHP_EOL)->toString(),
'memory_used' => str($info)->after('memory_used:')->before(PHP_EOL)->toString(),
'memory_free' => str($info)->after('memory_free:')->before(PHP_EOL)->toString(),
'disk_total' => str($info)->after('disk_total:')->before(PHP_EOL)->toString(),
'disk_used' => str($info)->after('disk_used:')->before(PHP_EOL)->toString(),
'disk_free' => str($info)->after('disk_free:')->before(PHP_EOL)->toString(),
];
}
}

View File

@ -0,0 +1,19 @@
# Update package lists
sudo DEBIAN_FRONTEND=noninteractive apt-get update -y
# Remove unnecessary dependencies
sudo DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y
# Clear package cache
sudo DEBIAN_FRONTEND=noninteractive apt-get clean -y
# Remove old configuration files
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y $(dpkg -l | grep '^rc' | awk '{print $2}')
# Clear temporary files
sudo rm -rf /tmp/*
# Clear journal logs
sudo DEBIAN_FRONTEND=noninteractive journalctl --vacuum-time=1d
echo "Cleanup completed."

View File

@ -1 +1 @@
[ -f __path__ ] && cat __path__
[ -f __path__ ] && sudo cat __path__

View File

@ -0,0 +1,7 @@
echo "load:$(uptime | awk -F'load average:' '{print $2}' | awk -F, '{print $1}' | tr -d ' ')"
echo "memory_total:$(free -k | awk 'NR==2{print $2}')"
echo "memory_used:$(free -k | awk 'NR==2{print $3}')"
echo "memory_free:$(free -k | awk 'NR==2{print $7}')"
echo "disk_total:$(df -BM / | awk 'NR==2{print $2}' | sed 's/M//')"
echo "disk_used:$(df -BM / | awk 'NR==2{print $3}' | sed 's/M//')"
echo "disk_free:$(df -BM / | awk 'NR==2{print $4}' | sed 's/M//')"

View File

@ -0,0 +1 @@
sudo tail -n __lines__ __path__

View File

@ -0,0 +1,42 @@
<?php
namespace App\SSH\Services;
use App\Models\Service;
abstract class AbstractService implements ServiceInterface
{
public function __construct(protected Service $service)
{
}
public function creationRules(array $input): array
{
return [];
}
public function creationData(array $input): array
{
return [];
}
public function deletionRules(): array
{
return [];
}
public function data(): array
{
return [];
}
public function install(): void
{
//
}
public function uninstall(): void
{
//
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\SSH\Services\AddOnServices;
use App\SSH\Services\ServiceInterface;
abstract class AbstractAddOnService implements ServiceInterface
{
abstract public function creationRules(array $input): array;
abstract public function creationData(array $input): array;
abstract public function create(): void;
abstract public function delete(): void;
abstract public function data(): array;
}

View File

@ -2,40 +2,78 @@
namespace App\SSH\Services\Database;
use App\Enums\BackupStatus;
use App\Models\BackupFile;
use App\Models\Server;
use App\Models\Service;
use App\SSH\HasScripts;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
abstract class AbstractDatabase implements Database, ServiceInterface
abstract class AbstractDatabase extends AbstractService implements Database
{
use HasScripts;
protected Service $service;
protected Server $server;
abstract protected function getScriptsDir(): string;
public function __construct(Service $service)
public function creationRules(array $input): array
{
$this->service = $service;
$this->server = $service->server;
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
$databaseExists = $this->service->server->database();
if ($databaseExists) {
$fail('You already have a database service on the server.');
}
},
],
];
}
public function install(): void
{
$version = $this->service->version;
$command = $this->getScript($this->service->name.'/install-'.$version.'.sh');
$this->server->ssh()->exec($command, 'install-'.$this->service->name.'-'.$version);
$status = $this->server->systemd()->status($this->service->unit);
$this->service->server->ssh()->exec($command, 'install-'.$this->service->name.'-'.$version);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasDatabase = $this->service->server->databases()->exists();
if ($hasDatabase) {
$fail('You have database(s) on the server.');
}
$hasDatabaseUser = $this->service->server->databaseUsers()->exists();
if ($hasDatabaseUser) {
$fail('You have database user(s) on the server.');
}
$hasRunningBackup = $this->service->server->backups()
->where('status', BackupStatus::RUNNING)
->exists();
if ($hasRunningBackup) {
$fail('You have database backup(s) on the server.');
}
},
],
];
}
public function uninstall(): void
{
$version = $this->service->version;
$command = $this->getScript($this->service->name.'/uninstall.sh');
$this->service->server->ssh()->exec($command, 'uninstall-'.$this->service->name.'-'.$version);
$this->service->server->os()->cleanup();
}
public function create(string $name): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/create.sh', [
'name' => $name,
]),
@ -45,7 +83,7 @@ public function create(string $name): void
public function delete(string $name): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/delete.sh', [
'name' => $name,
]),
@ -55,7 +93,7 @@ public function delete(string $name): void
public function createUser(string $username, string $password, string $host): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/create-user.sh', [
'username' => $username,
'password' => $password,
@ -67,7 +105,7 @@ public function createUser(string $username, string $password, string $host): vo
public function deleteUser(string $username, string $host): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/delete-user.sh', [
'username' => $username,
'host' => $host,
@ -78,7 +116,7 @@ public function deleteUser(string $username, string $host): void
public function link(string $username, string $host, array $databases): void
{
$ssh = $this->server->ssh();
$ssh = $this->service->server->ssh();
foreach ($databases as $database) {
$ssh->exec(
@ -94,7 +132,7 @@ public function link(string $username, string $host, array $databases): void
public function unlink(string $username, string $host): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/unlink.sh', [
'username' => $username,
'host' => $host,
@ -106,7 +144,7 @@ public function unlink(string $username, string $host): void
public function runBackup(BackupFile $backupFile): void
{
// backup
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/backup.sh', [
'file' => $backupFile->name,
'database' => $backupFile->backup->database->name,
@ -115,13 +153,13 @@ public function runBackup(BackupFile $backupFile): void
);
// upload to storage
$upload = $backupFile->backup->storage->provider()->ssh($this->server)->upload(
$upload = $backupFile->backup->storage->provider()->ssh($this->service->server)->upload(
$backupFile->path(),
$backupFile->storagePath(),
);
// cleanup
$this->server->ssh()->exec('rm '.$backupFile->name.'.zip');
$this->service->server->ssh()->exec('rm '.$backupFile->name.'.zip');
$backupFile->size = $upload['size'];
$backupFile->save();
@ -130,12 +168,12 @@ public function runBackup(BackupFile $backupFile): void
public function restoreBackup(BackupFile $backupFile, string $database): void
{
// download
$backupFile->backup->storage->provider()->ssh($this->server)->download(
$backupFile->backup->storage->provider()->ssh($this->service->server)->download(
$backupFile->storagePath(),
$backupFile->name.'.zip',
);
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/restore.sh', [
'database' => $database,
'file' => $backupFile->name,

View File

@ -9,4 +9,6 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server mariadb-backup -y
sudo systemctl unmask mysql.service
sudo service mysql start

View File

@ -9,4 +9,6 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server mariadb-backup -y
sudo systemctl unmask mysql.service
sudo service mysql start

View File

@ -0,0 +1,9 @@
sudo service mysql stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove mariadb-server mariadb-backup -y
sudo rm -rf /etc/mysql
sudo rm -rf /var/lib/mysql
sudo rm -rf /var/log/mysql
sudo rm -rf /var/run/mysqld
sudo rm -rf /var/run/mysqld/mysqld.sock

View File

@ -1,5 +1,7 @@
sudo DEBIAN_FRONTEND=noninteractive apt-get install mysql-server -y
sudo systemctl unmask mysql.service
sudo service mysql enable
sudo service mysql start

View File

@ -6,6 +6,8 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mysql-server -y
sudo systemctl unmask mysql.service
sudo service mysql enable
sudo service mysql start

View File

@ -0,0 +1,9 @@
sudo service mysql stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove mysql-server -y
sudo rm -rf /etc/mysql
sudo rm -rf /var/lib/mysql
sudo rm -rf /var/log/mysql
sudo rm -rf /var/run/mysqld
sudo rm -rf /var/run/mysqld/mysqld.sock

View File

@ -0,0 +1,11 @@
sudo service postgresql stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove postgresql-* -y
sudo rm -rf /etc/postgresql
sudo rm -rf /var/lib/postgresql
sudo rm -rf /var/log/postgresql
sudo rm -rf /var/run/postgresql
sudo rm -rf /var/run/postgresql/postmaster.pid
sudo rm -rf /var/run/postgresql/.s.PGSQL.5432
sudo rm -rf /var/run/postgresql/.s.PGSQL.5432.lock

View File

@ -2,15 +2,8 @@
namespace App\SSH\Services\Firewall;
use App\Models\Service;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
abstract class AbstractFirewall implements Firewall, ServiceInterface
abstract class AbstractFirewall extends AbstractService implements Firewall
{
protected Service $service;
public function __construct(Service $service)
{
$this->service = $service;
}
}

View File

@ -14,6 +14,12 @@ public function install(): void
$this->getScript('ufw/install-ufw.sh'),
'install-ufw'
);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
{
//
}
public function addRule(string $type, string $protocol, int $port, string $source, ?string $mask): void

View File

@ -0,0 +1,53 @@
<?php
namespace App\SSH\Services\Monitoring\RemoteMonitor;
use App\Models\Metric;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Validation\Rule;
class RemoteMonitor extends AbstractService
{
public function creationRules(array $input): array
{
return [
'type' => [
function (string $attribute, mixed $value, Closure $fail) {
$monitoringExists = $this->service->server->monitoring();
if ($monitoringExists) {
$fail('You already have a monitoring service on the server.');
}
},
],
'version' => [
'required',
Rule::in(['latest']),
],
];
}
public function creationData(array $input): array
{
return [
'data_retention' => 10,
];
}
public function data(): array
{
return [
'data_retention' => $this->service->type_data['data_retention'] ?? 10,
];
}
public function install(): void
{
//
}
public function uninstall(): void
{
Metric::where('server_id', $this->service->server_id)->delete();
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\SSH\Services\Monitoring\VitoAgent;
use App\Models\Metric;
use App\SSH\HasScripts;
use App\SSH\Services\AbstractService;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\Rule;
use Ramsey\Uuid\Uuid;
class VitoAgent extends AbstractService
{
use HasScripts;
const TAGS_URL = 'https://api.github.com/repos/vitodeploy/agent/tags';
const DOWNLOAD_URL = 'https://github.com/vitodeploy/agent/releases/download/%s';
public function creationRules(array $input): array
{
return [
'type' => [
Rule::unique('services', 'type')->where('server_id', $this->service->server_id),
],
'version' => [
'required',
Rule::in(['latest']),
],
];
}
public function creationData(array $input): array
{
return [
'url' => '',
'secret' => Uuid::uuid4()->toString(),
'data_retention' => 10,
];
}
public function data(): array
{
return [
'url' => $this->service->type_data['url'] ?? null,
'secret' => $this->service->type_data['secret'] ?? null,
'data_retention' => $this->service->type_data['data_retention'] ?? 10,
];
}
public function install(): void
{
$tags = Http::get(self::TAGS_URL)->json();
if (empty($tags)) {
throw new \Exception('Failed to fetch tags');
}
$this->service->version = $tags[0]['name'];
$this->service->save();
$downloadUrl = sprintf(self::DOWNLOAD_URL, $this->service->version);
$data = $this->data();
$data['url'] = route('api.servers.agent', [$this->service->server, $this->service->id]);
$this->service->type_data = $data;
$this->service->save();
$this->service->refresh();
$this->service->server->ssh()->exec(
$this->getScript('install.sh', [
'download_url' => $downloadUrl,
'config_url' => $this->data()['url'],
'config_secret' => $this->data()['secret'],
]),
'install-vito-agent'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('uninstall.sh'),
'uninstall-vito-agent'
);
Metric::where('server_id', $this->service->server_id)->delete();
}
}

View File

@ -0,0 +1,53 @@
arch=$(uname -m)
if [ "$arch" == "x86_64" ]; then
executable="vitoagent-linux-amd64"
elif [ "$arch" == "i686" ]; then
executable="vitoagent-linux-amd"
elif [ "$arch" == "armv7l" ]; then
executable="vitoagent-linux-arm"
elif [ "$arch" == "aarch64" ]; then
executable="vitoagent-linux-arm64"
else
executable="vitoagent-linux-amd64"
fi
wget __download_url__/$executable
chmod +x ./$executable
sudo mv ./$executable /usr/local/bin/vito-agent
# create service
export VITO_AGENT_SERVICE="
[Unit]
Description=Vito Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/vito-agent
Restart=on-failure
[Install]
WantedBy=multi-user.target
"
echo "${VITO_AGENT_SERVICE}" | sudo tee /etc/systemd/system/vito-agent.service
sudo mkdir -p /etc/vito-agent
export VITO_AGENT_CONFIG="
{
\"url\": \"__config_url__\",
\"secret\": \"__config_secret__\"
}
"
echo "${VITO_AGENT_CONFIG}" | sudo tee /etc/vito-agent/config.json
sudo systemctl daemon-reload
sudo systemctl enable vito-agent
sudo systemctl start vito-agent
echo "Vito Agent installed successfully"

View File

@ -0,0 +1,13 @@
sudo service vito-agent stop
sudo systemctl disable vito-agent
sudo rm -f /usr/local/bin/vito-agent
sudo rm -f /etc/systemd/system/vito-agent.service
sudo rm -rf /etc/vito-agent
sudo systemctl daemon-reload
echo "Vito Agent uninstalled successfully"

View File

@ -3,20 +3,43 @@
namespace App\SSH\Services\PHP;
use App\Exceptions\SSHCommandError;
use App\Models\Service;
use App\SSH\HasScripts;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class PHP implements ServiceInterface
class PHP extends AbstractService
{
use HasScripts;
protected Service $service;
public function __construct(Service $service)
public function creationRules(array $input): array
{
$this->service = $service;
return [
'version' => [
'required',
Rule::in(config('core.php_versions')),
Rule::unique('services', 'version')
->where('type', 'php')
->where('server_id', $this->service->server_id),
],
];
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasSite = $this->service->server->sites()
->where('php_version', $this->service->version)
->exists();
if ($hasSite) {
$fail('Some sites are using this PHP version.');
}
},
],
];
}
public function install(): void
@ -29,6 +52,7 @@ public function install(): void
]),
'install-php-'.$this->service->version
);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
@ -39,6 +63,7 @@ public function uninstall(): void
]),
'uninstall-php-'.$this->service->version
);
$this->service->server->os()->cleanup();
}
public function setDefaultCli(): void

View File

@ -2,15 +2,37 @@
namespace App\SSH\Services\ProcessManager;
use App\Models\Service;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
abstract class AbstractProcessManager implements ProcessManager, ServiceInterface
abstract class AbstractProcessManager extends AbstractService implements ProcessManager
{
protected Service $service;
public function creationRules(array $input): array
{
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
$processManagerExists = $this->service->server->processManager();
if ($processManagerExists) {
$fail('You already have a process manager service on the server.');
}
},
],
];
}
public function __construct(Service $service)
public function deletionRules(): array
{
$this->service = $service;
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasQueue = $this->service->server->queues()->exists();
if ($hasQueue) {
$fail('You have queue(s) on the server.');
}
},
],
];
}
}

View File

@ -15,6 +15,18 @@ public function install(): void
$this->getScript('supervisor/install-supervisor.sh'),
'install-supervisor'
);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('supervisor/uninstall-supervisor.sh'),
'uninstall-supervisor'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}
/**

View File

@ -0,0 +1,8 @@
sudo service supervisor stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove supervisor -y
sudo rm -rf /etc/supervisor
sudo rm -rf /var/log/supervisor
sudo rm -rf /var/run/supervisor
sudo rm -rf /var/run/supervisor/supervisor.sock

View File

@ -2,16 +2,27 @@
namespace App\SSH\Services\Redis;
use App\Models\Service;
use App\SSH\HasScripts;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
class Redis implements ServiceInterface
class Redis extends AbstractService
{
use HasScripts;
public function __construct(protected Service $service)
public function creationRules(array $input): array
{
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
$redisExists = $this->service->server->memoryDatabase();
if ($redisExists) {
$fail('You already have a Redis service on the server.');
}
},
],
];
}
public function install(): void
@ -20,5 +31,17 @@ public function install(): void
$this->getScript('install.sh'),
'install-redis'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('uninstall.sh'),
'uninstall-redis'
);
$this->service->server->os()->cleanup();
}
}

View File

@ -0,0 +1,15 @@
sudo service redis stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove redis-server -y
sudo rm -rf /etc/redis
sudo rm -rf /var/lib/redis
sudo rm -rf /var/log/redis
sudo rm -rf /var/run/redis
sudo rm -rf /var/run/redis/redis-server.pid
sudo rm -rf /var/run/redis/redis-server.sock
sudo rm -rf /var/run/redis/redis-server.sock
sudo DEBIAN_FRONTEND=noninteractive sudo apt-get autoremove -y
sudo DEBIAN_FRONTEND=noninteractive sudo apt-get autoclean -y

View File

@ -4,5 +4,15 @@
interface ServiceInterface
{
public function creationRules(array $input): array;
public function creationData(array $input): array;
public function deletionRules(): array;
public function data(): array;
public function install(): void;
public function uninstall(): void;
}

View File

@ -2,12 +2,8 @@
namespace App\SSH\Services\Webserver;
use App\Models\Service;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
abstract class AbstractWebserver implements ServiceInterface, Webserver
{
public function __construct(protected Service $service)
abstract class AbstractWebserver extends AbstractService implements Webserver
{
}
}

View File

@ -6,6 +6,7 @@
use App\Models\Site;
use App\Models\Ssl;
use App\SSH\HasScripts;
use Closure;
use Illuminate\Support\Str;
use Throwable;
@ -23,6 +24,31 @@ public function install(): void
]),
'install-nginx'
);
$this->service->server->os()->cleanup();
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasSite = $this->service->server->sites()
->exists();
if ($hasSite) {
$fail('Cannot uninstall webserver while you have websites using it.');
}
},
],
];
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('nginx/uninstall-nginx.sh'),
'uninstall-nginx'
);
$this->service->server->os()->cleanup();
}
public function createVHost(Site $site): void
@ -44,7 +70,7 @@ public function updateVHost(Site $site, bool $noSSL = false, ?string $vhost = nu
$this->getScript('nginx/update-vhost.sh', [
'domain' => $site->domain,
'path' => $site->path,
'vhost' => $this->generateVhost($site, $noSSL),
'vhost' => $vhost ?? $this->generateVhost($site, $noSSL),
]),
'update-vhost',
$site->id

View File

@ -0,0 +1,12 @@
sudo service nginx stop
sudo DEBIAN_FRONTEND=noninteractive apt-get purge nginx nginx-common nginx-full -y
sudo rm -rf /etc/nginx
sudo rm -rf /var/log/nginx
sudo rm -rf /var/lib/nginx
sudo rm -rf /var/cache/nginx
sudo rm -rf /usr/share/nginx
sudo rm -rf /etc/systemd/system/nginx.service
sudo systemctl daemon-reload

View File

@ -4,6 +4,7 @@
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
abstract class AbstractType implements ServerType
{
@ -31,7 +32,9 @@ public function install(): void
$service->update(['status' => ServiceStatus::READY]);
if ($service->type == 'php') {
$this->progress($currentProgress, 'installing-composer');
$service->handler()->installComposer();
/** @var PHP $handler */
$handler = $service->handler();
$handler->installComposer();
}
}
$this->progress(100, 'finishing');

View File

@ -13,7 +13,7 @@ public function createRules(array $input): array
],
'php' => [
'required',
'in:'.implode(',', config('core.php_versions')),
'in:none,'.implode(',', config('core.php_versions')),
],
'database' => [
'required',

View File

@ -32,5 +32,19 @@ function htmx(): HtmxResponse
function vito_version(): string
{
return exec('git describe --tags');
$version = exec('git describe --tags');
if (str($version)->contains('-')) {
return str($version)->before('-').' (dev)';
}
return $version;
}
function convert_time_format($string): string
{
$string = preg_replace('/(\d+)m/', '$1 minutes', $string);
$string = preg_replace('/(\d+)s/', '$1 seconds', $string);
$string = preg_replace('/(\d+)d/', '$1 days', $string);
return preg_replace('/(\d+)h/', '$1 hours', $string);
}

View File

@ -1,35 +1,5 @@
<?php
use App\Enums\OperatingSystem;
use App\Enums\StorageProvider;
use App\NotificationChannels\Discord;
use App\NotificationChannels\Email;
use App\NotificationChannels\Slack;
use App\NotificationChannels\Telegram;
use App\ServerProviders\AWS;
use App\ServerProviders\DigitalOcean;
use App\ServerProviders\Hetzner;
use App\ServerProviders\Linode;
use App\ServerProviders\Vultr;
use App\SiteTypes\Laravel;
use App\SiteTypes\PHPBlank;
use App\SiteTypes\PHPMyAdmin;
use App\SiteTypes\PHPSite;
use App\SiteTypes\Wordpress;
use App\SourceControlProviders\Bitbucket;
use App\SourceControlProviders\Github;
use App\SourceControlProviders\Gitlab;
use App\SSH\Services\Database\Mariadb;
use App\SSH\Services\Database\Mysql;
use App\SSH\Services\Database\Postgresql;
use App\SSH\Services\Firewall\Ufw;
use App\SSH\Services\PHP\PHP;
use App\SSH\Services\ProcessManager\Supervisor;
use App\SSH\Services\Redis\Redis;
use App\SSH\Services\Webserver\Nginx;
use App\StorageProviders\Dropbox;
use App\StorageProviders\FTP;
return [
/*
* SSH
@ -44,12 +14,12 @@
* General
*/
'operating_systems' => [
OperatingSystem::UBUNTU20,
OperatingSystem::UBUNTU22,
\App\Enums\OperatingSystem::UBUNTU20,
\App\Enums\OperatingSystem::UBUNTU22,
],
'webservers' => ['none', 'nginx'],
'php_versions' => [
'none',
// 'none',
// '5.6',
'7.0',
'7.1',
@ -126,117 +96,104 @@
],
'server_providers_class' => [
\App\Enums\ServerProvider::CUSTOM => \App\ServerProviders\Custom::class,
\App\Enums\ServerProvider::AWS => AWS::class,
\App\Enums\ServerProvider::LINODE => Linode::class,
\App\Enums\ServerProvider::DIGITALOCEAN => DigitalOcean::class,
\App\Enums\ServerProvider::VULTR => Vultr::class,
\App\Enums\ServerProvider::HETZNER => Hetzner::class,
\App\Enums\ServerProvider::AWS => \App\ServerProviders\AWS::class,
\App\Enums\ServerProvider::LINODE => \App\ServerProviders\Linode::class,
\App\Enums\ServerProvider::DIGITALOCEAN => \App\ServerProviders\DigitalOcean::class,
\App\Enums\ServerProvider::VULTR => \App\ServerProviders\Vultr::class,
\App\Enums\ServerProvider::HETZNER => \App\ServerProviders\Hetzner::class,
],
'server_providers_default_user' => [
'custom' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'aws' => [
'ubuntu_18' => 'ubuntu',
'ubuntu_20' => 'ubuntu',
'ubuntu_22' => 'ubuntu',
\App\Enums\OperatingSystem::UBUNTU20 => 'ubuntu',
\App\Enums\OperatingSystem::UBUNTU22 => 'ubuntu',
],
'linode' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'digitalocean' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'vultr' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'hetzner' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
],
/*
* Service
*/
'service_handlers' => [
'nginx' => Nginx::class,
'mysql' => Mysql::class,
'mariadb' => Mariadb::class,
'postgresql' => Postgresql::class,
'redis' => Redis::class,
'php' => PHP::class,
'ufw' => Ufw::class,
'supervisor' => Supervisor::class,
'service_types' => [
'nginx' => 'webserver',
'mysql' => 'database',
'mariadb' => 'database',
'postgresql' => 'database',
'redis' => 'memory_database',
'php' => 'php',
'ufw' => 'firewall',
'supervisor' => 'process_manager',
'vito-agent' => 'monitoring',
'remote-monitor' => 'monitoring',
],
'add_on_services' => [
// add-on services
'service_handlers' => [
'nginx' => \App\SSH\Services\Webserver\Nginx::class,
'mysql' => \App\SSH\Services\Database\Mysql::class,
'mariadb' => \App\SSH\Services\Database\Mariadb::class,
'postgresql' => \App\SSH\Services\Database\Postgresql::class,
'redis' => \App\SSH\Services\Redis\Redis::class,
'php' => \App\SSH\Services\PHP\PHP::class,
'ufw' => \App\SSH\Services\Firewall\Ufw::class,
'supervisor' => \App\SSH\Services\ProcessManager\Supervisor::class,
'vito-agent' => \App\SSH\Services\Monitoring\VitoAgent\VitoAgent::class,
'remote-monitor' => \App\SSH\Services\Monitoring\RemoteMonitor\RemoteMonitor::class,
],
'service_units' => [
'nginx' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'nginx',
],
'ubuntu_20' => [
'latest' => 'nginx',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'nginx',
],
],
'mysql' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'5.7' => 'mysql',
'8.0' => 'mysql',
],
'ubuntu_20' => [
'5.7' => 'mysql',
'8.0' => 'mysql',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'5.7' => 'mysql',
'8.0' => 'mysql',
],
],
'mariadb' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
'ubuntu_20' => [
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
],
'postgresql' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'12' => 'postgresql',
'13' => 'postgresql',
'14' => 'postgresql',
'15' => 'postgresql',
'16' => 'postgresql',
],
'ubuntu_20' => [
'12' => 'postgresql',
'13' => 'postgresql',
'14' => 'postgresql',
'15' => 'postgresql',
'16' => 'postgresql',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'12' => 'postgresql',
'13' => 'postgresql',
'14' => 'postgresql',
@ -245,19 +202,7 @@
],
],
'php' => [
'ubuntu_18' => [
'5.6' => 'php5.6-fpm',
'7.0' => 'php7.0-fpm',
'7.1' => 'php7.1-fpm',
'7.2' => 'php7.2-fpm',
'7.3' => 'php7.3-fpm',
'7.4' => 'php7.4-fpm',
'8.0' => 'php8.0-fpm',
'8.1' => 'php8.1-fpm',
'8.2' => 'php8.2-fpm',
'8.3' => 'php8.3-fpm',
],
'ubuntu_20' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'5.6' => 'php5.6-fpm',
'7.0' => 'php7.0-fpm',
'7.1' => 'php7.1-fpm',
@ -268,7 +213,7 @@
'8.1' => 'php8.1-fpm',
'8.3' => 'php8.3-fpm',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'5.6' => 'php5.6-fpm',
'7.0' => 'php7.0-fpm',
'7.1' => 'php7.1-fpm',
@ -282,36 +227,35 @@
],
],
'redis' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'redis',
],
'ubuntu_20' => [
'latest' => 'redis',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'redis',
],
],
'supervisor' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'supervisor',
],
'ubuntu_20' => [
'latest' => 'supervisor',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'supervisor',
],
],
'ufw' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'ufw',
],
'ubuntu_20' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'ufw',
],
'ubuntu_22' => [
'latest' => 'ufw',
],
'vito-agent' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'vito-agent',
],
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'vito-agent',
],
],
],
@ -327,11 +271,11 @@
\App\Enums\SiteType::PHPMYADMIN,
],
'site_types_class' => [
\App\Enums\SiteType::PHP => PHPSite::class,
\App\Enums\SiteType::PHP_BLANK => PHPBlank::class,
\App\Enums\SiteType::LARAVEL => Laravel::class,
\App\Enums\SiteType::WORDPRESS => Wordpress::class,
\App\Enums\SiteType::PHPMYADMIN => PHPMyAdmin::class,
\App\Enums\SiteType::PHP => \App\SiteTypes\PHPSite::class,
\App\Enums\SiteType::PHP_BLANK => \App\SiteTypes\PHPBlank::class,
\App\Enums\SiteType::LARAVEL => \App\SiteTypes\Laravel::class,
\App\Enums\SiteType::WORDPRESS => \App\SiteTypes\Wordpress::class,
\App\Enums\SiteType::PHPMYADMIN => \App\SiteTypes\PHPMyAdmin::class,
],
/*
@ -343,9 +287,9 @@
'bitbucket',
],
'source_control_providers_class' => [
'github' => Github::class,
'gitlab' => Gitlab::class,
'bitbucket' => Bitbucket::class,
'github' => \App\SourceControlProviders\Github::class,
'gitlab' => \App\SourceControlProviders\Gitlab::class,
'bitbucket' => \App\SourceControlProviders\Bitbucket::class,
],
/*
@ -403,26 +347,33 @@
\App\Enums\NotificationChannel::TELEGRAM,
],
'notification_channels_providers_class' => [
\App\Enums\NotificationChannel::SLACK => Slack::class,
\App\Enums\NotificationChannel::DISCORD => Discord::class,
\App\Enums\NotificationChannel::EMAIL => Email::class,
\App\Enums\NotificationChannel::TELEGRAM => Telegram::class,
\App\Enums\NotificationChannel::SLACK => \App\NotificationChannels\Slack::class,
\App\Enums\NotificationChannel::DISCORD => \App\NotificationChannels\Discord::class,
\App\Enums\NotificationChannel::EMAIL => \App\NotificationChannels\Email::class,
\App\Enums\NotificationChannel::TELEGRAM => \App\NotificationChannels\Telegram::class,
],
/*
* storage providers
*/
'storage_providers' => [
StorageProvider::DROPBOX,
StorageProvider::FTP,
\App\Enums\StorageProvider::DROPBOX,
\App\Enums\StorageProvider::FTP,
],
'storage_providers_class' => [
'dropbox' => Dropbox::class,
'ftp' => FTP::class,
'dropbox' => \App\StorageProviders\Dropbox::class,
'ftp' => \App\StorageProviders\Ftp::class,
],
'ssl_types' => [
\App\Enums\SslType::LETSENCRYPT,
\App\Enums\SslType::CUSTOM,
],
'metrics_data_retention' => [
7,
14,
30,
90,
],
];

View File

@ -0,0 +1,22 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class MetricFactory extends Factory
{
public function definition(): array
{
return [
'server_id' => 1,
'load' => $this->faker->randomFloat(2, 0, 100),
'memory_total' => $this->faker->randomFloat(0, 0, 100),
'memory_used' => $this->faker->randomFloat(0, 0, 100),
'memory_free' => $this->faker->randomFloat(0, 0, 100),
'disk_total' => $this->faker->randomFloat(0, 0, 100),
'disk_used' => $this->faker->randomFloat(0, 0, 100),
'disk_free' => $this->faker->randomFloat(0, 0, 100),
];
}
}

View File

@ -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('server_logs', function (Blueprint $table) {
$table->boolean('is_remote')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_logs', function (Blueprint $table) {
$table->dropColumn('is_remote');
});
}
};

View File

@ -0,0 +1,37 @@
<?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('metrics', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('server_id');
$table->decimal('load', 5, 2);
$table->decimal('memory_total', 15, 0);
$table->decimal('memory_used', 15, 0);
$table->decimal('memory_free', 15, 0);
$table->decimal('disk_total', 15, 0);
$table->decimal('disk_used', 15, 0);
$table->decimal('disk_free', 15, 0);
$table->timestamps();
$table->index(['server_id', 'created_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('metrics');
}
};

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Seeders;
use App\Models\Metric;
use App\Models\Service;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Database\Seeder;
class MetricsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Metric::query()->delete();
$monitoring = Service::query()
->where('type', 'monitoring')
->firstOrFail();
$range = CarbonPeriod::create(Carbon::now()->subDays(7), '1 minute', Carbon::now());
foreach ($range as $date) {
Metric::factory()->create([
'server_id' => $monitoring->server_id,
'created_at' => $date,
]);
}
}
}

View File

@ -1,25 +1,29 @@
ARG RELEASE=1
FROM ubuntu:22.04
ENV RELEASE_ARG=$RELEASE
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND noninteractive
# upgrade
RUN apt clean && apt update && apt update && apt upgrade -y && apt autoremove -y
RUN apt-get clean && apt-get update && apt-get update && apt-get upgrade -y && apt-get autoremove -y
# requirements
RUN apt install -y software-properties-common curl zip unzip git gcc
RUN apt-get install -y software-properties-common curl zip unzip git gcc
# nginx
RUN apt install -y nginx
RUN apt-get install -y nginx
# php
RUN apt update \
&& apt install -y gnupg gosu curl ca-certificates zip unzip git supervisor libcap2-bin libpng-dev \
RUN apt-get update \
&& apt-get install -y cron gnupg gosu curl ca-certificates zip unzip git supervisor libcap2-bin libpng-dev \
python2 dnsutils librsvg2-bin fswatch wget \
&& add-apt-repository ppa:ondrej/php -y \
&& apt update \
&& apt install -y php8.2 php8.2-fpm php8.2-mbstring php8.2-mcrypt php8.2-gd php8.2-xml \
&& apt-get update \
&& apt-get install -y php8.2 php8.2-fpm php8.2-mbstring php8.2-mcrypt php8.2-gd php8.2-xml \
php8.2-curl php8.2-gettext php8.2-zip php8.2-bcmath php8.2-soap php8.2-redis php8.2-sqlite3
COPY docker/php.ini /etc/php/8.2/cli/conf.d/99-vito.ini
@ -29,7 +33,7 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
# app
RUN rm -rf /var/www/html
RUN git clone -b 1.x https://github.com/vitodeploy/vito.git /var/www/html
RUN git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
RUN [ "$RELEASE_ARG" = "1" ] && git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1) || true
RUN composer install --no-dev --prefer-dist
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache
@ -40,6 +44,8 @@ RUN rm /etc/nginx/sites-enabled/default
COPY docker/nginx.conf /etc/nginx/sites-available/default
RUN ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
RUN echo "* * * * * cd /var/www/html && php artisan schedule:run >> /var/log/cron.log 2>&1" | crontab -
# supervisord
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

View File

@ -37,6 +37,8 @@ php /var/www/html/artisan view:cache
php /var/www/html/artisan user:create "$NAME" "$EMAIL" "$PASSWORD"
cron
echo "Vito is running! 🚀"
/usr/bin/supervisord

239
package-lock.json generated
View File

@ -8,8 +8,10 @@
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2",
"apexcharts": "^3.44.2",
"autoprefixer": "^10.4.2",
"flowbite": "^2.3.0",
"flowbite-datepicker": "^1.2.6",
"htmx.org": "^1.9.10",
"laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2",
@ -20,7 +22,7 @@
"tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4",
"vite": "^4.5.2"
"vite": "^4.5.3"
}
},
"node_modules/@esbuild/android-arm": {
@ -529,6 +531,12 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"dev": true
},
"node_modules/@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"dev": true
},
"node_modules/alpinejs": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.12.0.tgz",
@ -557,6 +565,21 @@
"node": ">= 8"
}
},
"node_modules/apexcharts": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz",
"integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==",
"dev": true,
"dependencies": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -885,6 +908,15 @@
"mini-svg-data-uri": "^1.4.3"
}
},
"node_modules/flowbite-datepicker": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.2.6.tgz",
"integrity": "sha512-UbU/xXs9HFiwWfL4M1vpwIo8EpS0NUQSOvYnp0Z9u3N118nU7lPFGoUOq7su9d0aOJy9FssXzx1SZwN8MXhE1g==",
"dev": true,
"dependencies": {
"flowbite": "^2.0.0"
}
},
"node_modules/fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@ -1680,6 +1712,97 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dev": true,
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
"dev": true,
"dependencies": {
"svg.js": ">=2.3.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
"dev": true,
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==",
"dev": true
},
"node_modules/svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"dev": true,
"dependencies": {
"svg.js": "^2.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dev": true,
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dev": true,
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dev": true,
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tailwindcss": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz",
@ -1812,9 +1935,9 @@
"dev": true
},
"node_modules/vite": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
@ -2177,6 +2300,12 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"dev": true
},
"@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"dev": true
},
"alpinejs": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.12.0.tgz",
@ -2202,6 +2331,21 @@
"picomatch": "^2.0.4"
}
},
"apexcharts": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz",
"integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==",
"dev": true,
"requires": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -2434,6 +2578,15 @@
"mini-svg-data-uri": "^1.4.3"
}
},
"flowbite-datepicker": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.2.6.tgz",
"integrity": "sha512-UbU/xXs9HFiwWfL4M1vpwIo8EpS0NUQSOvYnp0Z9u3N118nU7lPFGoUOq7su9d0aOJy9FssXzx1SZwN8MXhE1g==",
"dev": true,
"requires": {
"flowbite": "^2.0.0"
}
},
"fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@ -2911,6 +3064,78 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dev": true,
"requires": {
"svg.js": "^2.0.1"
}
},
"svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
"dev": true,
"requires": {
"svg.js": ">=2.3.x"
}
},
"svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
"dev": true,
"requires": {
"svg.js": "^2.2.5"
}
},
"svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==",
"dev": true
},
"svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"dev": true,
"requires": {
"svg.js": "^2.4.0"
}
},
"svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dev": true,
"requires": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"dependencies": {
"svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dev": true,
"requires": {
"svg.js": "^2.2.5"
}
}
}
},
"svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dev": true,
"requires": {
"svg.js": "^2.6.5"
}
},
"tailwindcss": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz",
@ -3011,9 +3236,9 @@
"dev": true
},
"vite": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",

View File

@ -22,6 +22,8 @@
"tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4",
"vite": "^4.5.2"
"vite": "^4.5.3",
"apexcharts": "^3.44.2",
"flowbite-datepicker": "^1.2.6"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-e3775b0a.css",
"file": "assets/app-f65997bb.css",
"isEntry": true,
"src": "resources/css/app.css"
},
@ -12,7 +12,7 @@
"css": [
"assets/app-a1ae07b3.css"
],
"file": "assets/app-5f99a92f.js",
"file": "assets/app-66009dff.js",
"isEntry": true,
"src": "resources/js/app.js"
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -5.23 70 70" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg">
<metadata>
<rdf:RDF>
<cc:Work>
<dc:subject>
Miscellaneous
</dc:subject>
<dc:identifier>
health-monitoring
</dc:identifier>
<dc:title>
Health Monitoring
</dc:title>
<dc:format>
image/svg+xml
</dc:format>
<dc:publisher>
Amido Limited
</dc:publisher>
<dc:creator>
Richard Slater
</dc:creator>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<path d="m -633.94123,753.25889 c -6.59942,-3.2916 -17.80605,-13.7307 -24.90952,-23.2035 l -0.38611,-0.5149 4.90905,0 c 3.3284,0 5.08031,-0.051 5.44092,-0.1594 0.95525,-0.2862 1.50799,-0.9179 2.58607,-2.9554 1.97619,-3.735 2.24879,-4.2224 2.2879,-4.0904 0.0218,0.074 0.44604,4.3009 0.94276,9.3939 0.87326,8.9538 0.91529,9.2823 1.27154,9.9368 0.62081,1.1407 1.47439,1.6301 2.85312,1.6359 1.01617,0 1.76269,-0.3415 2.41627,-1.1191 0.25355,-0.3016 1.82033,-3.2056 3.48173,-6.4532 l 3.02073,-5.9047 10.36659,-0.039 c 10.32236,-0.039 10.36894,-0.041 10.91581,-0.3356 1.1802,-0.6369 1.77594,-1.6202 1.77528,-2.9304 -6.9e-4,-1.3721 -0.67396,-2.4208 -1.91258,-2.9791 -0.5125,-0.231 -1.30161,-0.2501 -11.80218,-0.2858 -7.69785,-0.026 -11.47959,0.01 -11.97032,0.1108 -1.27206,0.264 -1.77303,0.7868 -3.0106,3.1416 l -1.08999,2.0739 -0.1043,-0.5158 c -0.0574,-0.2837 -0.47667,-4.3775 -0.9318,-9.0974 -0.45513,-4.7199 -0.88563,-8.7992 -0.95668,-9.0652 -0.36496,-1.3662 -1.62876,-2.2659 -3.16688,-2.2544 -1.04822,0.01 -1.94772,0.4395 -2.48617,1.1931 -0.17485,0.2447 -1.92936,3.5346 -3.8989,7.311 l -3.581,6.866 -5.76782,0.036 -5.76783,0.036 -0.83086,-1.6834 c -2.06318,-4.1804 -2.89449,-7.6097 -2.738,-11.2949 0.12425,-2.9261 0.69392,-5.0125 2.04328,-7.4832 1.10812,-2.029 3.06519,-4.3559 4.69277,-5.5795 1.78333,-1.3407 4.15216,-2.2461 6.64618,-2.5403 2.10735,-0.2485 4.60651,0.089 7.37391,0.9964 1.2153,0.3984 4.21499,1.9073 5.62954,2.8318 2.45012,1.6012 5.68511,4.4633 7.84072,6.9369 l 0.80955,0.929 0.94007,-1.2397 c 1.88483,-2.4857 4.78785,-5.1075 7.55221,-6.8208 5.19337,-3.2187 11.05786,-4.2791 15.6703,-2.8335 3.74959,1.1752 6.7744,3.9944 8.98105,8.3706 2.19828,4.3596 2.39398,9.8576 0.53892,15.1404 -1.06649,3.0372 -2.39805,5.6594 -4.46756,8.7979 -2.55838,3.88 -4.87538,6.6471 -9.08862,10.8542 -5.31708,5.3093 -11.00984,9.9038 -16.48777,13.3068 -1.60577,0.9976 -3.84246,2.2037 -4.0818,2.201 -0.0583,0 -0.75536,-0.325 -1.54898,-0.7208 z" fill="#00bcf2" transform="translate(667.003 -694.43)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,22 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="800" rx="50" fill="#5046E5" />
<g filter="url(#filter0_d_1_35)">
<path
d="M400 601.2C392.4 601.2 386.2 599.4 381.4 595.8C376.6 592.2 372.8 586.8 370 579.6L205 202.2C202.2 195.4 201.4 189.8 202.6 185.4C203.8 180.6 206.4 176.8 210.4 174C214.8 171.2 219.8 169.8 225.4 169.8C233 169.8 238.6 171.6 242.2 175.2C246.2 178.4 249.4 183.2 251.8 189.6L410.2 557.4H391L548.8 189C551.6 183 555 178.4 559 175.2C563 171.6 568.6 169.8 575.8 169.8C581.4 169.8 586 171.2 589.6 174C593.6 176.8 596 180.6 596.8 185.4C598 190.2 597.2 195.8 594.4 202.2L429.4 579.6C426.6 586.8 422.8 592.2 418 595.8C413.6 599.4 407.6 601.2 400 601.2Z"
fill="white" />
</g>
<defs>
<filter id="filter0_d_1_35" x="196.8" y="169.8" width="405.8" height="439.4" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_35" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_35" result="shape" />
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
ace.define("ace/mode/sh", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text", "ace/tokenizer", "ace/mode/sh_highlight_rules", "ace/range"], function (e, t, n) { var r = e("../lib/oop"), i = e("./text").Mode, s = e("../tokenizer").Tokenizer, o = e("./sh_highlight_rules").ShHighlightRules, u = e("../range").Range, a = function () { this.$tokenizer = new s((new o).getRules()) }; r.inherits(a, i), function () { this.toggleCommentLines = function (e, t, n, r) { var i = !0, s = /^(\s*)#/; for (var o = n; o <= r; o++)if (!s.test(t.getLine(o))) { i = !1; break } if (i) { var a = new u(0, 0, 0, 0); for (var o = n; o <= r; o++) { var f = t.getLine(o), l = f.match(s); a.start.row = o, a.end.row = o, a.end.column = l[0].length, t.replace(a, l[1]) } } else t.indentRows(n, r, "#") }, this.getNextLineIndent = function (e, t, n) { var r = this.$getIndent(t), i = this.$tokenizer.getLineTokens(t, e), s = i.tokens; if (s.length && s[s.length - 1].type == "comment") return r; if (e == "start") { var o = t.match(/^.*[\{\(\[\:]\s*$/); o && (r += n) } return r }; var e = { pass: 1, "return": 1, raise: 1, "break": 1, "continue": 1 }; this.checkOutdent = function (t, n, r) { if (r !== "\r\n" && r !== "\r" && r !== "\n") return !1; var i = this.$tokenizer.getLineTokens(n.trim(), t).tokens; if (!i) return !1; do var s = i.pop(); while (s && (s.type == "comment" || s.type == "text" && s.value.match(/^\s+$/))); return s ? s.type == "keyword" && e[s.value] : !1 }, this.autoOutdent = function (e, t, n) { n += 1; var r = this.$getIndent(t.getLine(n)), i = t.getTabString(); r.slice(-i.length) == i && t.remove(new u(n, r.length - i.length, n, r.length)) } }.call(a.prototype), t.Mode = a }), ace.define("ace/mode/sh_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function (e, t, n) { var r = e("../lib/oop"), i = e("./text_highlight_rules").TextHighlightRules, s = t.reservedKeywords = "!|{|}|case|do|done|elif|else|esac|fi|for|if|in|then|until|while|&|;|export|local|read|typeset|unset|elif|select|set", o = t.languageConstructs = "[|]|alias|bg|bind|break|builtin|cd|command|compgen|complete|continue|dirs|disown|echo|enable|eval|exec|exit|fc|fg|getopts|hash|help|history|jobs|kill|let|logout|popd|printf|pushd|pwd|return|set|shift|shopt|source|suspend|test|times|trap|type|ulimit|umask|unalias|wait", u = function () { var e = this.createKeywordMapper({ keyword: s, "support.function.builtin": o, "invalid.deprecated": "debugger" }, "identifier"), t = "(?:(?:[1-9]\\d*)|(?:0))", n = "(?:\\.\\d+)", r = "(?:\\d+)", i = "(?:(?:" + r + "?" + n + ")|(?:" + r + "\\.))", u = "(?:(?:" + i + "|" + r + ")" + ")", a = "(?:" + u + "|" + i + ")", f = "(?:&" + r + ")", l = "[a-zA-Z][a-zA-Z0-9_]*", c = "(?:(?:\\$" + l + ")|(?:" + l + "=))", h = "(?:\\$(?:SHLVL|\\$|\\!|\\?))", p = "(?:" + l + "\\s*\\(\\))"; this.$rules = { start: [{ token: "comment", regex: "#.*$" }, { token: "string", regex: '"(?:[^\\\\]|\\\\.)*?"' }, { token: "variable.language", regex: h }, { token: "variable", regex: c }, { token: "support.function", regex: p }, { token: "support.function", regex: f }, { token: "string", regex: "'(?:[^\\\\]|\\\\.)*?'" }, { token: "constant.numeric", regex: a }, { token: "constant.numeric", regex: t + "\\b" }, { token: e, regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" }, { token: "keyword.operator", regex: "\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|~|<|>|<=|=>|=|!=" }, { token: "paren.lparen", regex: "[\\[\\(\\{]" }, { token: "paren.rparen", regex: "[\\]\\)\\}]" }, { token: "text", regex: "\\s+" }] } }; r.inherits(u, i), t.ShHighlightRules = u })

View File

@ -1,5 +0,0 @@
ace.define("ace/theme/github", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) {
t.isDark = !1, t.cssClass = "ace-github", t.cssText = '/* CSS style content from github\'s default pygments highlighter template.Cursor and selection styles from textmate.css. */.ace-github .ace_gutter {background: #e8e8e8;color: #AAA;}.ace-github .ace_scroller {background: #fff;}.ace-github .ace_keyword {font-weight: bold;}.ace-github .ace_string {color: #D14;}.ace-github .ace_variable.ace_class {color: teal;}.ace-github .ace_constant.ace_numeric {color: #099;}.ace-github .ace_constant.ace_buildin {color: #0086B3;}.ace-github .ace_support.ace_function {color: #0086B3;}.ace-github .ace_comment {color: #998;font-style: italic;}.ace-github .ace_variable.ace_language {color: #0086B3;}.ace-github .ace_paren {font-weight: bold;}.ace-github .ace_boolean {font-weight: bold;}.ace-github .ace_string.ace_regexp {color: #009926;font-weight: normal;}.ace-github .ace_variable.ace_instance {color: teal;}.ace-github .ace_constant.ace_language {font-weight: bold;}.ace-github .ace_text-layer {}.ace-github .ace_cursor {border-left: 2px solid black;}.ace-github .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid black;}.ace-github .ace_marker-layer .ace_active-line {background: rgb(255, 255, 204);}.ace-github .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-github.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;border-radius: 2px;}/* bold keywords cause cursor issues for some fonts *//* this disables bold style for editor and keeps for static highlighter */.ace-github.ace_nobold .ace_line > span {font-weight: normal !important;}.ace-github .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-github .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-github .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-github .ace_gutter-active-line {background-color : rgba(0, 0, 0, 0.07);}.ace-github .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-github .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-github .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';
var r = e("../lib/dom");
r.importCssString(t.cssText, t.cssClass)
})

View File

@ -1,5 +0,0 @@
ace.define("ace/theme/one-dark", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) {
t.isDark = !1, t.cssClass = "ace-one-dark", t.cssText = '/* CSS style content from one-dark\'s default pygments highlighter template.Cursor and selection styles from textmate.css. */.ace-one-dark .ace_gutter{background:#282c34;color:#6a6f7a}.ace-one-dark .ace_print-margin{width:1px;background:#e8e8e8}.ace-one-dark{background-color:#282c34;color:#abb2bf}.ace-one-dark .ace_cursor{color:#528bff}.ace-one-dark .ace_marker-layer .ace_selection{background:#3d4350}.ace-one-dark.ace_multiselect .ace_selection.ace_start{box-shadow:0 0 3px 0 #282c34;border-radius:2px}.ace-one-dark .ace_marker-layer .ace_step{background:#c6dbae}.ace-one-dark .ace_marker-layer .ace_bracket{margin:-1px 0 0 -1px;border:1px solid #747369}.ace-one-dark .ace_marker-layer .ace_active-line{background:rgba(76,87,103,.19)}.ace-one-dark .ace_gutter-active-line{background-color:rgba(76,87,103,.19)}.ace-one-dark .ace_marker-layer .ace_selected-word{border:1px solid #3d4350}.ace-one-dark .ace_fold{background-color:#61afef;border-color:#abb2bf}.ace-one-dark .ace_keyword{color:#c678dd}.ace-one-dark .ace_keyword.ace_operator{color:#c678dd}.ace-one-dark .ace_keyword.ace_other.ace_unit{color:#d19a66}.ace-one-dark .ace_constant.ace_language{color:#d19a66}.ace-one-dark .ace_constant.ace_numeric{color:#d19a66}.ace-one-dark .ace_constant.ace_character{color:#56b6c2}.ace-one-dark .ace_constant.ace_other{color:#56b6c2}.ace-one-dark .ace_support.ace_function{color:#61afef}.ace-one-dark .ace_support.ace_constant{color:#d19a66}.ace-one-dark .ace_support.ace_class{color:#e5c07b}.ace-one-dark .ace_support.ace_type{color:#e5c07b}.ace-one-dark .ace_storage{color:#c678dd}.ace-one-dark .ace_storage.ace_type{color:#c678dd}.ace-one-dark .ace_invalid{color:#fff;background-color:#f2777a}.ace-one-dark .ace_invalid.ace_deprecated{color:#272b33;background-color:#d27b53}.ace-one-dark .ace_string{color:#98c379}.ace-one-dark .ace_string.ace_regexp{color:#e06c75}.ace-one-dark .ace_comment{font-style:italic;color:#5c6370}.ace-one-dark .ace_variable{color:#e06c75}.ace-one-dark .ace_variable.ace_parameter{color:#d19a66}.ace-one-dark .ace_meta.ace_tag{color:#e06c75}.ace-one-dark .ace_entity.ace_other.ace_attribute-name{color:#e06c75}.ace-one-dark .ace_entity.ace_name.ace_function{color:#61afef}.ace-one-dark .ace_entity.ace_name.ace_tag{color:#e06c75}.ace-one-dark .ace_markup.ace_heading{color:#98c379}.ace-one-dark .ace_indent-guide{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ09NrYAgMjP4PAAtGAwchHMyAAAAAAElFTkSuQmCC) right repeat-y}';
var r = e("../lib/dom");
r.importCssString(t.cssText, t.cssClass)
})

View File

@ -1,9 +1,13 @@
import 'flowbite';
import 'flowbite/dist/datepicker.js';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
import ApexCharts from 'apexcharts';
window.ApexCharts = ApexCharts;
import htmx from "htmx.org";
window.htmx = htmx;
window.htmx.defineExtension('disable-element', {
@ -23,12 +27,10 @@ window.htmx.defineExtension('disable-element', {
});
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRF-TOKEN'] = document.head.querySelector('meta[name="csrf-token"]').content;
if (window.getSelection) { window.getSelection().removeAllRanges(); }
else if (document.selection) { document.selection.empty(); }
// if (window.getSelection) { window.getSelection().removeAllRanges(); }
// else if (document.selection) { document.selection.empty(); }
});
let activeElement = null;
document.body.addEventListener('htmx:beforeRequest', (event) => {
activeElement = document.activeElement;
let targetElements = event.target.querySelectorAll('[hx-disable]');
for (let i = 0; i < targetElements.length; i++) {
targetElements[i].disabled = true;
@ -46,11 +48,6 @@ document.body.addEventListener('htmx:afterSwap', (event) => {
return reference.getAttribute('data-tooltip');
},
});
if (activeElement) {
activeElement.blur();
activeElement.focus();
activeElement = null;
}
});
import toastr from 'toastr';

View File

@ -4,6 +4,7 @@
<x-slot name="trigger">
<x-secondary-button>
{{ __("Auto Deployment") }}
<x-heroicon name="o-chevron-down" class="ml-1 h-4 w-4" />
</x-secondary-button>
</x-slot>
<x-slot name="content">

View File

@ -6,8 +6,9 @@
hx-swap="outerHTML"
hx-select="#deploy"
>
<x-primary-button hx-disable>
<x-primary-button class="flex items-center justify-between" :active="true" hx-disable>
{{ __("Deploy") }}
<x-heroicon name="o-play-circle" class="ml-1 h-5 w-5" />
</x-primary-button>
</form>
@endif

View File

@ -12,11 +12,13 @@ class="p-6"
{{ __("Deployment Script") }}
</h2>
<div class="mt-6">A bash script that will be executed when you run the deployment process.</div>
<div class="mt-6">
<x-input-label for="script" :value="__('Script')" />
<x-code-editor id="script" name="script" lang="sh" class="mt-1 w-full">
<x-textarea id="script" name="script" class="mt-1 min-h-[400px] w-full">
{{ old("script", $site->deploymentScript?->content) }}
</x-code-editor>
</x-textarea>
@error("script")
<x-input-error class="mt-2" :messages="$message" />
@enderror

View File

@ -21,9 +21,9 @@ class="mt-6"
>
<x-input-label for="env" :value="__('.env')" />
<div id="env-content">
<x-code-editor id="env" name="env" rows="10" class="mt-1 block w-full">
<x-textarea id="env" name="env" rows="10" class="mt-1 block min-h-[400px] w-full">
{{ old("env", session()->get("env") ?? "Loading...") }}
</x-code-editor>
</x-textarea>
</div>
@error("env")
<x-input-error class="mt-2" :messages="$message" />

View File

@ -19,6 +19,7 @@
<x-slot name="trigger">
<x-secondary-button>
{{ __("Manage") }}
<x-heroicon name="o-chevron-down" class="ml-1 h-4 w-4" />
</x-secondary-button>
</x-slot>
<x-slot name="content">

View File

@ -0,0 +1,104 @@
@props([
"id",
"type",
"title",
"color",
"sets",
"categories",
"toolbar" => false,
"formatter" => null,
])
<x-simple-card {{ $attributes }}>
<div class="relative">
<div class="absolute left-4 top-4">{{ $title }}</div>
</div>
<div id="{{ $id }}" class="pt-4"></div>
<script>
window.addEventListener('load', function () {
let options = {
series: [
@foreach ($sets as $set)
{
name: '{{ $set["name"] }}',
data: @json($set["data"]),
color: '{{ $set["color"] }}'
},
@endforeach
],
chart: {
height: '100%',
maxWidth: '100%',
type: '{{ $type }}',
fontFamily: 'Inter, sans-serif',
dropShadow: {
enabled: false
},
toolbar: {
show: @js($toolbar)
}
},
tooltip: {
enabled: true,
x: {
show: true
}
},
legend: {
show: true
},
@if ($type == 'area')
fill: {
type: 'gradient',
gradient: {
opacityFrom: 0.55,
opacityTo: 0,
shade: '{{ $color }}',
gradientToColors: ['{{ $color }}']
}
},
@endif
dataLabels: {
enabled: false
},
stroke: {
width: 2,
curve: 'smooth'
},
grid: {
show: false,
strokeDashArray: 4,
padding: {
left: 2,
right: 2,
top: 0
}
},
xaxis: {
categories: @json($categories),
labels: {
show: false
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false,
labels: {
@if ($formatter)
formatter: {{ $formatter }},
@endif
}
}
};
if (document.getElementById('{{ $id }}') && typeof ApexCharts !== 'undefined') {
const chart = new ApexCharts(document.getElementById('{{ $id }}'), options);
chart.render();
}
});
</script>
</x-simple-card>

View File

@ -1,45 +0,0 @@
@props([
"id",
"name",
"disabled" => false,
"lang" => "text",
])
<div
x-data="{
editorId: @js($id),
disabled: @js($disabled),
lang: @js($lang),
init() {
document.body.addEventListener('htmx:afterSettle', (event) => {
let editor = null
let theme =
document.documentElement.className === 'dark'
? 'one-dark'
: 'github'
editor = window.ace.edit(this.editorId, {})
let contentElement = document.getElementById(
`text-${this.editorId}`,
)
editor.setValue(contentElement.innerText, 1)
if (this.disabled) {
editor.setReadOnly(true)
}
editor.getSession().setMode(`ace/mode/${this.lang}`)
editor.setTheme(`ace/theme/${theme}`)
editor.setFontSize('15px')
editor.setShowPrintMargin(false)
editor.on('change', () => {
contentElement.innerHTML = editor.getValue()
})
document.body.addEventListener('color-scheme-changed', (event) => {
theme = event.detail.theme === 'dark' ? 'one-dark' : 'github'
editor.setTheme(`ace/theme/${theme}`)
})
})
},
}"
>
<div id="{{ $id }}" class="min-h-[400px] w-full rounded-md border border-gray-200 dark:border-gray-700"></div>
<textarea id="text-{{ $id }}" name="{{ $name }}" class="hidden">{{ $slot }}</textarea>
</div>

View File

@ -1,10 +1,3 @@
@php
$class = "mx-auto px-4 sm:px-6 lg:px-8";
if (! str($attributes->get("class"))->contains("max-w-")) {
$class .= " max-w-7xl";
}
@endphp
<div {!! $attributes->merge(["class" => $class]) !!}>
<div {!! $attributes->merge(["class" => "max-w-5xl mx-auto"]) !!}>
{{ $slot }}
</div>

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 599 B

Some files were not shown because too many files have changed in this diff Show More