This commit is contained in:
Joao Patricio 2023-03-20 12:04:22 +00:00
parent a0da981ec7
commit 75326f6626
27 changed files with 1770 additions and 95 deletions

View File

@ -1,30 +0,0 @@
FROM serversideup/php:8.2-fpm-nginx
ARG NODE_VERSION=18
ARG POSTGRES_VERSION=15
RUN apt-get update \
&& curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g npm
RUN apt-get install -y php-pgsql openssh-client
RUN apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
# Adding Coolify User and SSH key to connect to the Testing-Host (which is simulation of a VPS)
RUN useradd -ms /bin/bash coolify
USER coolify
RUN mkdir -p ~/.ssh
RUN echo "-----BEGIN OPENSSH PRIVATE KEY-----\n\
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n\
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk\n\
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA\n\
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV\n\
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==\n\
-----END OPENSSH PRIVATE KEY-----" >> ~/.ssh/id_ed25519
RUN chmod 0600 ~/.ssh/id_ed25519
USER root

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class RunCommand extends Component
{
public $activity;
public $isKeepAliveOn = false;
public $manualKeepAlive = false;
public $command = '';
public function render()
{
return view('livewire.run-command');
}
public function runCommand()
{
// TODO Execute with Livewire Normally
$this->activity = coolifyProcess($this->command, 'testing-host');
// Override manual to experiment
// $sleepingBeauty = 'x=1; while [ $x -le 40 ]; do sleep 0.1 && echo "Welcome $x times" $(( x++ )); done';
//
// $commandString = <<<EOT
// cd projects/dummy-project
// ~/.docker/cli-plugins/docker-compose build --no-cache
// # $sleepingBeauty
// EOT;
//
// $this->activity = coolifyProcess($commandString, 'testing-host');
$this->isKeepAliveOn = true;
}
public function polling()
{
$this->activity?->refresh();
if ($this->activity?->properties['status'] === 'finished') {
$this->isKeepAliveOn = false;
}
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace App\Jobs;
use Illuminate\Support\Facades\DB;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Process\InvokedProcess;
use Illuminate\Process\ProcessResult;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Contracts\Activity;
class ExecuteCoolifyProcess implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $throttleIntervalMS = 500;
protected $timeStart;
protected $currentTime;
protected $lastWriteAt = 0;
protected string $stdOutIncremental = '';
protected string $stdErrIncremental = '';
/**
* Create a new job instance.
*/
public function __construct(
public Activity $activity,
){}
/**
* Execute the job.
*/
public function handle(): ?ProcessResult
{
ray()->clearAll();
$this->timeStart = $start = hrtime(true);
$user = $this->activity->getExtraProperty('user');
$destination = $this->activity->getExtraProperty('destination');
$port = $this->activity->getExtraProperty('port');
$command = $this->activity->getExtraProperty('command');
$delimiter = 'EOF-COOLIFY-SSH';
File::chmod(base_path('coolify_id25519'), '0600');
$sshCommand = 'ssh '
. '-i ./coolify_id25519'
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no '
. "{$user}@{$destination} "
. " 'bash -se' << \\$delimiter" . PHP_EOL
. $command . PHP_EOL
. $delimiter;
// $sshCommand = "whoami ; pwd ; ls ";
$process = Process::start(
$sshCommand,
$this->handleOutput(...),
);
$res = $process->wait();
if (app()->environment('testing')) {
return $res;
}
// TODO Why is this not persisting?? Immutable property??
$this->activity->properties->put('pid', $process->id());
$this->activity->properties->put('exitCode', $res->exitCode());
$this->activity->properties->put('stdout', $res->output());
$this->activity->properties->put('stderr', $res->errorOutput());
$this->activity->save();
}
protected function handleOutput(string $type, string $output)
{
$this->currentTime = $this->elapsedTime();
if ($type === 'out') {
$this->stdOutIncremental .= $output;
} else {
$this->stdErrIncremental .= $output;
}
$this->activity->description .= $output;
if ($this->isAfterLastThrottle()) {
// Let's write to database.
DB::transaction(function () {
$this->activity->save();
$this->lastWriteAt = $this->currentTime;
});
}
}
/**
* Decides if it's time to write again to database.
*
* @return bool
*/
protected function isAfterLastThrottle()
{
// If DB was never written, then we immediately decide we have to write.
if ($this->lastWriteAt === 0) {
return true;
}
return ($this->currentTime - $this->throttleIntervalMS) > $this->lastWriteAt;
}
protected function elapsedTime(): int
{
$timeMs = (hrtime(true) - $this->timeStart) / 1_000_000;
return intval($timeMs);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Services;
use App\Jobs\ExecuteCoolifyProcess;
use Illuminate\Process\ProcessResult;
use Spatie\Activitylog\Contracts\Activity;
class CoolifyProcess
{
protected Activity $activity;
// TODO Left 'root' as default user instead of 'coolify' because
// there's a task at TODO.md to run docker without sudo
public function __construct(
protected string $destination,
protected string $command,
protected ?int $port = 22,
protected ?string $user = 'root',
){
$this->activity = activity()
->withProperty('type', 'COOLIFY_PROCESS')
->withProperty('user', $this->user)
->withProperty('destination', $this->destination)
->withProperty('port', $this->port)
->withProperty('command', $this->command)
->withProperty('status', ProcessStatus::HOLDING)
->log("Awaiting to start command...\n\n");
}
public function __invoke(): Activity|ProcessResult
{
$job = new ExecuteCoolifyProcess($this->activity);
if (app()->environment('testing')) {
return $job->handle();
}
dispatch($job);
return $this->activity;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Services;
enum ProcessStatus: string
{
case HOLDING = 'holding';
case IN_PROGRESS = 'in_progress';
case FINISHED = 'finished';
case ERROR = 'error';
}

259
app/Services/Ssh.php Normal file
View File

@ -0,0 +1,259 @@
<?php
namespace App\Services;
use Closure;
use Exception;
use Symfony\Component\Process\Process;
/**
* Started from a copy of spatie/ssh
*/
class Ssh
{
protected string $user;
protected string $host;
protected array $extraOptions = [];
protected Closure $processConfigurationClosure;
protected Closure $onOutput;
public function __construct(string $user, string $host, int $port = null)
{
$this->user = $user;
$this->host = $host;
if ($port !== null) {
$this->usePort($port);
}
$this->processConfigurationClosure = fn(Process $process) => null;
$this->onOutput = fn($type, $line) => null;
}
public static function create(...$args): self
{
return new static(...$args);
}
public function usePrivateKey(string $pathToPrivateKey): self
{
$this->extraOptions['private_key'] = '-i ' . $pathToPrivateKey;
return $this;
}
public function useJumpHost(string $jumpHost): self
{
$this->extraOptions['jump_host'] = '-J ' . $jumpHost;
return $this;
}
public function usePort(int $port): self
{
if ($port < 0) {
throw new Exception('Port must be a positive integer.');
}
$this->extraOptions['port'] = '-p ' . $port;
return $this;
}
public function useMultiplexing(string $controlPath, string $controlPersist = '10m'): self
{
$this->extraOptions['control_master'] = '-o ControlMaster=auto -o ControlPath=' . $controlPath . ' -o ControlPersist=' . $controlPersist;
return $this;
}
public function configureProcess(Closure $processConfigurationClosure): self
{
$this->processConfigurationClosure = $processConfigurationClosure;
return $this;
}
public function onOutput(Closure $onOutput): self
{
$this->onOutput = $onOutput;
return $this;
}
public function enableStrictHostKeyChecking(): self
{
unset($this->extraOptions['enable_strict_check']);
return $this;
}
public function disableStrictHostKeyChecking(): self
{
$this->extraOptions['enable_strict_check'] = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
return $this;
}
public function enableQuietMode(): self
{
$this->extraOptions['quiet'] = '-q';
return $this;
}
public function disableQuietMode(): self
{
unset($this->extraOptions['quiet']);
return $this;
}
public function disablePasswordAuthentication(): self
{
$this->extraOptions['password_authentication'] = '-o PasswordAuthentication=no';
return $this;
}
public function enablePasswordAuthentication(): self
{
unset($this->extraOptions['password_authentication']);
return $this;
}
public function addExtraOption(string $option): self
{
$this->extraOptions[] = $option;
return $this;
}
/**
* @param string|array $command
*
* @return string
*/
public function getExecuteCommand($command): string
{
$commands = $this->wrapArray($command);
$extraOptions = implode(' ', $this->getExtraOptions());
$commandString = implode(PHP_EOL, $commands);
$delimiter = 'EOF-SPATIE-SSH';
$target = $this->getTargetForSsh();
if (in_array($this->host, ['local', 'localhost', '127.0.0.1'])) {
return $commandString;
}
return "ssh {$extraOptions} {$target} 'bash -se' << \\$delimiter" . PHP_EOL
. $commandString . PHP_EOL
. $delimiter;
}
/**
* @param string|array $command
*
* @return \Symfony\Component\Process\Process
**/
public function execute($command): Process
{
$sshCommand = $this->getExecuteCommand($command);
return $this->run($sshCommand);
}
/**
* @param string|array $command
*
* @return \Symfony\Component\Process\Process
*/
public function executeAsync($command): Process
{
$sshCommand = $this->getExecuteCommand($command);
return $this->run($sshCommand, 'start');
}
public function getDownloadCommand(string $sourcePath, string $destinationPath): string
{
return "scp {$this->getExtraScpOptions()} {$this->getTargetForScp()}:$sourcePath $destinationPath";
}
public function download(string $sourcePath, string $destinationPath): Process
{
$downloadCommand = $this->getDownloadCommand($sourcePath, $destinationPath);
return $this->run($downloadCommand);
}
public function getUploadCommand(string $sourcePath, string $destinationPath): string
{
return "scp {$this->getExtraScpOptions()} $sourcePath {$this->getTargetForScp()}:$destinationPath";
}
public function upload(string $sourcePath, string $destinationPath): Process
{
$uploadCommand = $this->getUploadCommand($sourcePath, $destinationPath);
return $this->run($uploadCommand);
}
protected function getExtraScpOptions(): string
{
$extraOptions = $this->extraOptions;
if (isset($extraOptions['port'])) {
$extraOptions['port'] = str_replace('-p', '-P', $extraOptions['port']);
}
$extraOptions[] = '-r';
return implode(' ', array_values($extraOptions));
}
private function getExtraOptions(): array
{
return array_values($this->extraOptions);
}
protected function wrapArray($arrayOrString): array
{
return (array)$arrayOrString;
}
protected function run(string $command, string $method = 'run'): Process
{
$process = Process::fromShellCommandline($command);
$process->setTimeout(0);
($this->processConfigurationClosure)($process);
$process->{$method}($this->onOutput);
return $process;
}
protected function getTargetForScp(): string
{
$host = filter_var($this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '[' . $this->host . ']' : $this->host;
return "{$this->user}@{$host}";
}
protected function getTargetForSsh(): string
{
return "{$this->user}@{$this->host}";
}
}

24
bootstrap/helpers.php Normal file
View File

@ -0,0 +1,24 @@
<?php
use App\Services\CoolifyProcess;
use Illuminate\Process\ProcessResult;
use Spatie\Activitylog\Contracts\Activity;
if (! function_exists('coolifyProcess')) {
/**
* Run a Coolify Process, which SSH's into a machine to run the command(s).
*
*/
function coolifyProcess($command, $destination): Activity|ProcessResult
{
$process = resolve(CoolifyProcess::class, [
'destination' => $destination,
'command' => $command,
]);
$activityLog = $process();
return $activityLog;
}
}

View File

@ -9,7 +9,10 @@
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8"
"laravel/tinker": "^2.8",
"livewire/livewire": "^2.12",
"spatie/laravel-activitylog": "^4.7",
"spatie/laravel-ray": "^1.32"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
@ -21,6 +24,9 @@
"spatie/laravel-ignition": "^2.0"
},
"autoload": {
"files": [
"bootstrap/helpers.php"
],
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",

966
composer.lock generated

File diff suppressed because it is too large Load Diff

108
config/ray.php Normal file
View File

@ -0,0 +1,108 @@
<?php
return [
/*
* This setting controls whether data should be sent to Ray.
*
* By default, `ray()` will only transmit data in non-production environments.
*/
'enable' => env('RAY_ENABLED', true),
/*
* When enabled, all cache events will automatically be sent to Ray.
*/
'send_cache_to_ray' => env('SEND_CACHE_TO_RAY', false),
/*
* When enabled, all things passed to `dump` or `dd`
* will be sent to Ray as well.
*/
'send_dumps_to_ray' => env('SEND_DUMPS_TO_RAY', true),
/*
* When enabled all job events will automatically be sent to Ray.
*/
'send_jobs_to_ray' => env('SEND_JOBS_TO_RAY', false),
/*
* When enabled, all things logged to the application log
* will be sent to Ray as well.
*/
'send_log_calls_to_ray' => env('SEND_LOG_CALLS_TO_RAY', true),
/*
* When enabled, all queries will automatically be sent to Ray.
*/
'send_queries_to_ray' => env('SEND_QUERIES_TO_RAY', false),
/**
* When enabled, all duplicate queries will automatically be sent to Ray.
*/
'send_duplicate_queries_to_ray' => env('SEND_DUPLICATE_QUERIES_TO_RAY', false),
/*
* When enabled, slow queries will automatically be sent to Ray.
*/
'send_slow_queries_to_ray' => env('SEND_SLOW_QUERIES_TO_RAY', false),
/**
* Queries that are longer than this number of milliseconds will be regarded as slow.
*/
'slow_query_threshold_in_ms' => env('RAY_SLOW_QUERY_THRESHOLD_IN_MS', 500),
/*
* When enabled, all requests made to this app will automatically be sent to Ray.
*/
'send_requests_to_ray' => env('SEND_REQUESTS_TO_RAY', false),
/**
* When enabled, all Http Client requests made by this app will be automatically sent to Ray.
*/
'send_http_client_requests_to_ray' => env('SEND_HTTP_CLIENT_REQUESTS_TO_RAY', false),
/*
* When enabled, all views that are rendered automatically be sent to Ray.
*/
'send_views_to_ray' => env('SEND_VIEWS_TO_RAY', false),
/*
* When enabled, all exceptions will be automatically sent to Ray.
*/
'send_exceptions_to_ray' => env('SEND_EXCEPTIONS_TO_RAY', true),
/*
* When enabled, all deprecation notices will be automatically sent to Ray.
*/
'send_deprecated_notices_to_ray' => env('SEND_DEPRECATED_NOTICES_TO_RAY', false),
/*
* The host used to communicate with the Ray app.
* When using Docker on Mac or Windows, you can replace localhost with 'host.docker.internal'
* When using Docker on Linux, you can replace localhost with '172.17.0.1'
* When using Homestead with the VirtualBox provider, you can replace localhost with '10.0.2.2'
* When using Homestead with the Parallels provider, you can replace localhost with '10.211.55.2'
*/
'host' => env('RAY_HOST', 'host.docker.internal'),
/*
* The port number used to communicate with the Ray app.
*/
'port' => env('RAY_PORT', 23517),
/*
* Absolute base path for your sites or projects in Homestead,
* Vagrant, Docker, or another remote development server.
*/
'remote_path' => env('RAY_REMOTE_PATH', null),
/*
* Absolute base path for your sites or projects on your local
* computer where your IDE or code editor is running on.
*/
'local_path' => env('RAY_LOCAL_PATH', null),
/*
* When this setting is enabled, the package will not try to format values sent to Ray.
*/
'always_send_raw_values' => false,
];

7
coolify_id25519 Normal file
View File

@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
-----END OPENSSH PRIVATE KEY-----

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('log_name')->nullable();
$table->text('description');
$table->nullableMorphs('subject', 'subject');
$table->nullableMorphs('causer', 'causer');
$table->json('properties')->nullable();
$table->timestamps();
$table->index('log_name');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name'));
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEventColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->string('event')->nullable()->after('subject_type');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('event');
});
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBatchUuidColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->uuid('batch_uuid')->nullable()->after('properties');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('batch_uuid');
});
}
}

View File

@ -0,0 +1,32 @@
<?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('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
}
};

View File

@ -3,8 +3,8 @@ services:
php:
image: coolify:4
build:
context: .
dockerfile: Dockerfile
context: ./docker/dev
ports:
- "${APP_PORT:-8000}:80"
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}"

20
docker/dev/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM serversideup/php:8.2-fpm-nginx
ARG NODE_VERSION=18
ARG POSTGRES_VERSION=15
RUN apt-get update \
&& curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g npm
RUN apt-get install -y php-pgsql openssh-client
RUN apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
USER root
# S6 Overlay config
COPY --chmod=755 etc/s6-overlay/ /etc/s6-overlay/

View File

@ -0,0 +1,2 @@
#!/command/execlineb -P
su webuser -c "php /var/www/html/artisan queue:listen --tries=3"

View File

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1,3 @@
<button wire:loading.remove {{ $attributes }} class="btn btn-primary rounded-none btn-xs no-animation"> {{ $slot }} </button>
<button wire:loading class="btn btn-disabled rounded-none btn-xs no-animation"> {{ $slot }}</button>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="light" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? 'Coolify' }}</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/js/app.js', 'resources/css/app.css'])
@livewireStyles
</head>
<body class="h-full">
@auth
<x-navbar />
@endauth
<main class="h-full pt-10 p-4">
{{ $slot }}
</main>
@livewireScripts
</body>
</html>

View File

@ -0,0 +1,15 @@
<nav class="flex space-x-4 p-2 fixed right-0">
@env('local')
<a href="/run-command">Run command</a>
{{-- <livewire:create-token /> --}}
<a href="/debug">Debug</a>
<a href="/debug/logs" target="_blank">Debug Logs</a>
{{-- <a href="/debug/inertia">Debug(inertia)</a> --}}
@endenv
<a href="/">Dashboard</a>
<a href="/profile">Profile</a>
<form action="/logout" method="POST">
@csrf
<button type="submit">Logout</button>
</form>
</nav>

View File

@ -0,0 +1,5 @@
<x-layout>
<livewire:run-command />
</x-layout>

View File

@ -0,0 +1,44 @@
<div>
<div>
<div>
<label for="command">
<input class="ring-1" id="command" wire:model="command" type="text"/>
</label>
<button class="btn btn-success btn-xs rounded-none" wire:click="runCommand">
Run command
<button>
</div>
@isset($activity?->id)
<div>
Activity: <span>{{ $activity?->id ?? 'waiting' }}</span>
</div>
@endisset
</div>
<div class="w-full h-10"></div>
<pre
style="
background-color: #FFFFFF;
width: 1200px;
height: 600px;
overflow-y: scroll;
display: flex;
flex-direction: column-reverse;
"
placeholder="Build output"
@if($isKeepAliveOn || $manualKeepAlive) wire:poll.750ms="polling" @endif
>
{{ data_get($activity, 'description') }}
</pre>
<div>
<input id="manualKeepAlive" name="manualKeepAlive" type="checkbox" wire:model="manualKeepAlive">
<label for="manualKeepAlive"> Live content </label>
</div>
@if($isKeepAliveOn || $manualKeepAlive)
Polling...
@endif
</div>

View File

@ -13,6 +13,10 @@
<h1 class="text-3xl font-bold">
Hello from Coolify!
</h1>
<p class="mt-4">
<a href="/demo"> See demo </a>
</p>
</body>
</html>

View File

@ -16,3 +16,5 @@
Route::get('/', function () {
return view('welcome');
});
Route::view('/demo', 'demo');