v4.0.0-beta.457 (#7690)
This commit is contained in:
commit
4f1a210bd9
19 changed files with 577 additions and 278 deletions
|
|
@ -3176,8 +3176,7 @@ private function graceful_shutdown_container(string $containerName)
|
|||
try {
|
||||
$timeout = isDev() ? 1 : 30;
|
||||
$this->execute_remote_command(
|
||||
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
} catch (Exception $error) {
|
||||
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
|
||||
|
|
|
|||
|
|
@ -669,7 +669,7 @@ private function upload_to_s3(): void
|
|||
$this->add_to_error_output($e->getMessage());
|
||||
throw $e;
|
||||
} finally {
|
||||
$command = "docker rm -f backup-of-{$this->backup_log_uuid}";
|
||||
$command = "docker stop backup-of-{$this->backup_log_uuid}";
|
||||
instant_remote_process([$command], $this->server, true, false, null, disableMultiplexing: true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,16 +105,7 @@ public function polling()
|
|||
|
||||
public function getLogLinesProperty()
|
||||
{
|
||||
return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) {
|
||||
$logLine['line'] = e($logLine['line']);
|
||||
$logLine['line'] = preg_replace(
|
||||
'/(https?:\/\/[^\s]+)/',
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="underline text-neutral-400">$1</a>',
|
||||
$logLine['line'],
|
||||
);
|
||||
|
||||
return $logLine;
|
||||
});
|
||||
return decode_remote_command_output($this->application_deployment_queue);
|
||||
}
|
||||
|
||||
public function copyLogs(): string
|
||||
|
|
|
|||
182
app/Livewire/Server/Sentinel.php
Normal file
182
app/Livewire/Server/Sentinel.php
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Sentinel extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
|
||||
public array $parameters = [];
|
||||
|
||||
public bool $isMetricsEnabled;
|
||||
|
||||
#[Validate(['required'])]
|
||||
public string $sentinelToken;
|
||||
|
||||
public ?string $sentinelUpdatedAt = null;
|
||||
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int $sentinelMetricsRefreshRateSeconds;
|
||||
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int $sentinelMetricsHistoryDays;
|
||||
|
||||
#[Validate(['required', 'integer', 'min:10'])]
|
||||
public int $sentinelPushIntervalSeconds;
|
||||
|
||||
#[Validate(['nullable', 'url'])]
|
||||
public ?string $sentinelCustomUrl = null;
|
||||
|
||||
public bool $isSentinelEnabled;
|
||||
|
||||
public bool $isSentinelDebugEnabled;
|
||||
|
||||
public ?string $sentinelCustomDockerImage = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},SentinelRestarted" => 'handleSentinelRestarted',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->syncData();
|
||||
} catch (\Throwable) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->validate();
|
||||
$this->server->settings->is_metrics_enabled = $this->isMetricsEnabled;
|
||||
$this->server->settings->sentinel_token = $this->sentinelToken;
|
||||
$this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds;
|
||||
$this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays;
|
||||
$this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds;
|
||||
$this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl;
|
||||
$this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled;
|
||||
$this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled;
|
||||
$this->server->settings->save();
|
||||
} else {
|
||||
$this->isMetricsEnabled = $this->server->settings->is_metrics_enabled;
|
||||
$this->sentinelToken = $this->server->settings->sentinel_token;
|
||||
$this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds;
|
||||
$this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days;
|
||||
$this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds;
|
||||
$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;
|
||||
$this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled;
|
||||
$this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled;
|
||||
$this->sentinelUpdatedAt = $this->server->sentinel_updated_at;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleSentinelRestarted($event)
|
||||
{
|
||||
if ($event['serverUuid'] === $this->server->uuid) {
|
||||
$this->server->refresh();
|
||||
$this->syncData();
|
||||
$this->dispatch('success', 'Sentinel has been restarted successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
public function restartSentinel()
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageSentinel', $this->server);
|
||||
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
|
||||
$this->server->restartSentinel($customImage);
|
||||
$this->dispatch('info', 'Restarting Sentinel.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedIsSentinelDebugEnabled($value)
|
||||
{
|
||||
try {
|
||||
$this->submit();
|
||||
$this->restartSentinel();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedIsMetricsEnabled($value)
|
||||
{
|
||||
try {
|
||||
$this->submit();
|
||||
$this->restartSentinel();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedIsSentinelEnabled($value)
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageSentinel', $this->server);
|
||||
if ($value === true) {
|
||||
if ($this->server->isBuildServer()) {
|
||||
$this->isSentinelEnabled = false;
|
||||
$this->dispatch('error', 'Sentinel cannot be enabled on build servers.');
|
||||
|
||||
return;
|
||||
}
|
||||
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
|
||||
StartSentinel::run($this->server, true, null, $customImage);
|
||||
} else {
|
||||
$this->isMetricsEnabled = false;
|
||||
$this->isSentinelDebugEnabled = false;
|
||||
StopSentinel::dispatch($this->server);
|
||||
}
|
||||
$this->submit();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSentinelToken()
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageSentinel', $this->server);
|
||||
$this->server->settings->generateSentinelToken();
|
||||
$this->dispatch('success', 'Token regenerated. Restarting Sentinel.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Sentinel settings updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.sentinel');
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +81,8 @@ class Show extends Component
|
|||
|
||||
public ?int $selectedHetznerTokenId = null;
|
||||
|
||||
public ?string $manualHetznerServerId = null;
|
||||
|
||||
public ?array $matchedHetznerServer = null;
|
||||
|
||||
public ?string $hetznerSearchError = null;
|
||||
|
|
@ -447,6 +449,10 @@ public function handleServerValidated($event = null)
|
|||
|
||||
// Update validation state
|
||||
$this->isValidating = $this->server->is_validating ?? false;
|
||||
|
||||
// Reload Hetzner tokens in case the linking section should now be shown
|
||||
$this->loadHetznerTokens();
|
||||
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->dispatch('refreshServer');
|
||||
}
|
||||
|
|
@ -524,6 +530,47 @@ public function searchHetznerServer(): void
|
|||
}
|
||||
}
|
||||
|
||||
public function searchHetznerServerById(): void
|
||||
{
|
||||
$this->hetznerSearchError = null;
|
||||
$this->hetznerNoMatchFound = false;
|
||||
$this->matchedHetznerServer = null;
|
||||
|
||||
if (! $this->selectedHetznerTokenId) {
|
||||
$this->hetznerSearchError = 'Please select a Hetzner token first.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->manualHetznerServerId) {
|
||||
$this->hetznerSearchError = 'Please enter a Hetzner Server ID.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
|
||||
if (! $token) {
|
||||
$this->hetznerSearchError = 'Invalid token selected.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
$serverData = $hetznerService->getServer((int) $this->manualHetznerServerId);
|
||||
|
||||
if (! empty($serverData)) {
|
||||
$this->matchedHetznerServer = $serverData;
|
||||
} else {
|
||||
$this->hetznerNoMatchFound = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->hetznerSearchError = 'Failed to fetch Hetzner server: '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function linkToHetzner()
|
||||
{
|
||||
if (! $this->matchedHetznerServer) {
|
||||
|
|
@ -564,6 +611,7 @@ public function linkToHetzner()
|
|||
// Clear the linking state
|
||||
$this->matchedHetznerServer = null;
|
||||
$this->selectedHetznerTokenId = null;
|
||||
$this->manualHetznerServerId = null;
|
||||
$this->hetznerNoMatchFound = false;
|
||||
$this->hetznerSearchError = null;
|
||||
|
||||
|
|
|
|||
59
app/Livewire/Server/Swarm.php
Normal file
59
app/Livewire/Server/Swarm.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Swarm extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
|
||||
public array $parameters = [];
|
||||
|
||||
public bool $isSwarmManager;
|
||||
|
||||
public bool $isSwarmWorker;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->syncData();
|
||||
} catch (\Throwable) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->server->settings->is_swarm_manager = $this->isSwarmManager;
|
||||
$this->server->settings->is_swarm_worker = $this->isSwarmWorker;
|
||||
$this->server->settings->save();
|
||||
} else {
|
||||
$this->isSwarmManager = $this->server->settings->is_swarm_manager;
|
||||
$this->isSwarmWorker = $this->server->settings->is_swarm_worker;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.swarm');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.456',
|
||||
'version' => '4.0.0-beta.457',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.456"
|
||||
"version": "4.0.0-beta.457"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.457"
|
||||
"version": "4.0.0-beta.458"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.12"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@
|
|||
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
|
||||
</a>
|
||||
@endif
|
||||
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
|
||||
<a class="menu-item {{ $activeMenu === 'swarm' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.swarm', ['server_uuid' => $server->uuid]) }}">Swarm
|
||||
</a>
|
||||
@endif
|
||||
@if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer())
|
||||
<a class="menu-item {{ $activeMenu === 'sentinel' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.sentinel', ['server_uuid' => $server->uuid]) }}">Sentinel
|
||||
</a>
|
||||
@endif
|
||||
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -11,13 +11,8 @@
|
|||
rafId: null,
|
||||
showTimestamps: true,
|
||||
searchQuery: '',
|
||||
renderTrigger: 0,
|
||||
matchCount: 0,
|
||||
deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}',
|
||||
// Cache for decoded HTML to avoid repeated DOMParser calls
|
||||
decodeCache: new Map(),
|
||||
// Cache for match count to avoid repeated DOM queries
|
||||
matchCountCache: null,
|
||||
lastSearchQuery: '',
|
||||
makeFullscreen() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
},
|
||||
|
|
@ -31,7 +26,6 @@
|
|||
if (!this.alwaysScroll) return;
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
this.scrollToBottom();
|
||||
// Schedule next scroll after a reasonable delay (250ms instead of 100ms)
|
||||
if (this.alwaysScroll) {
|
||||
setTimeout(() => this.scheduleScroll(), 250);
|
||||
}
|
||||
|
|
@ -48,10 +42,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
matchesSearch(text) {
|
||||
if (!this.searchQuery.trim()) return true;
|
||||
return text.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
},
|
||||
hasActiveLogSelection() {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed || !selection.toString().trim()) {
|
||||
|
|
@ -63,86 +53,59 @@
|
|||
return logsContainer.contains(range.commonAncestorContainer);
|
||||
},
|
||||
decodeHtml(text) {
|
||||
// Return cached result if available
|
||||
if (this.decodeCache.has(text)) {
|
||||
return this.decodeCache.get(text);
|
||||
}
|
||||
// Decode HTML entities with max iteration limit
|
||||
let decoded = text;
|
||||
let prev = '';
|
||||
let iterations = 0;
|
||||
const maxIterations = 3;
|
||||
|
||||
while (decoded !== prev && iterations < maxIterations) {
|
||||
prev = decoded;
|
||||
const doc = new DOMParser().parseFromString(decoded, 'text/html');
|
||||
decoded = doc.documentElement.textContent;
|
||||
iterations++;
|
||||
}
|
||||
// Cache the result (limit cache size to prevent memory bloat)
|
||||
if (this.decodeCache.size > 5000) {
|
||||
// Clear oldest entries when cache gets too large
|
||||
const firstKey = this.decodeCache.keys().next().value;
|
||||
this.decodeCache.delete(firstKey);
|
||||
}
|
||||
this.decodeCache.set(text, decoded);
|
||||
return decoded;
|
||||
const doc = new DOMParser().parseFromString(text, 'text/html');
|
||||
return doc.documentElement.textContent;
|
||||
},
|
||||
renderHighlightedLog(el, text) {
|
||||
// Skip re-render if user has text selected in logs
|
||||
if (el.textContent && this.hasActiveLogSelection()) {
|
||||
return;
|
||||
}
|
||||
highlightText(el, text, query) {
|
||||
if (this.hasActiveLogSelection()) return;
|
||||
|
||||
const decoded = this.decodeHtml(text);
|
||||
el.textContent = '';
|
||||
|
||||
if (!this.searchQuery.trim()) {
|
||||
el.textContent = decoded;
|
||||
return;
|
||||
}
|
||||
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
const lowerText = decoded.toLowerCase();
|
||||
const lowerText = text.toLowerCase();
|
||||
let lastIndex = 0;
|
||||
|
||||
let index = lowerText.indexOf(query, lastIndex);
|
||||
|
||||
while (index !== -1) {
|
||||
if (index > lastIndex) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex, index)));
|
||||
el.appendChild(document.createTextNode(text.substring(lastIndex, index)));
|
||||
}
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'log-highlight';
|
||||
mark.textContent = decoded.substring(index, index + this.searchQuery.length);
|
||||
mark.textContent = text.substring(index, index + query.length);
|
||||
el.appendChild(mark);
|
||||
|
||||
lastIndex = index + this.searchQuery.length;
|
||||
lastIndex = index + query.length;
|
||||
index = lowerText.indexOf(query, lastIndex);
|
||||
}
|
||||
|
||||
if (lastIndex < decoded.length) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex)));
|
||||
if (lastIndex < text.length) {
|
||||
el.appendChild(document.createTextNode(text.substring(lastIndex)));
|
||||
}
|
||||
},
|
||||
getMatchCount() {
|
||||
if (!this.searchQuery.trim()) return 0;
|
||||
// Return cached count if search query hasn't changed
|
||||
if (this.lastSearchQuery === this.searchQuery && this.matchCountCache !== null) {
|
||||
return this.matchCountCache;
|
||||
}
|
||||
applySearch() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return 0;
|
||||
if (!logs) return;
|
||||
const lines = logs.querySelectorAll('[data-log-line]');
|
||||
const query = this.searchQuery.trim().toLowerCase();
|
||||
let count = 0;
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(query)) {
|
||||
count++;
|
||||
const content = (line.dataset.logContent || '').toLowerCase();
|
||||
const textSpan = line.querySelector('[data-line-text]');
|
||||
const matches = !query || content.includes(query);
|
||||
|
||||
line.classList.toggle('hidden', !matches);
|
||||
if (matches && query) count++;
|
||||
|
||||
if (textSpan) {
|
||||
const originalText = this.decodeHtml(textSpan.dataset.lineText || '');
|
||||
if (!query) {
|
||||
textSpan.textContent = originalText;
|
||||
} else if (matches) {
|
||||
this.highlightText(textSpan, originalText, query);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.matchCountCache = count;
|
||||
this.lastSearchQuery = this.searchQuery;
|
||||
return count;
|
||||
|
||||
this.matchCount = query ? count : 0;
|
||||
},
|
||||
downloadLogs() {
|
||||
const logs = document.getElementById('logs');
|
||||
|
|
@ -173,35 +136,30 @@
|
|||
}
|
||||
},
|
||||
init() {
|
||||
// Prevent Livewire from morphing logs container when text is selected
|
||||
Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => {
|
||||
if (el.id === 'logs' && this.hasActiveLogSelection()) {
|
||||
skip();
|
||||
// Watch search query changes
|
||||
this.$watch('searchQuery', () => {
|
||||
this.applySearch();
|
||||
});
|
||||
|
||||
// Apply search after Livewire updates
|
||||
Livewire.hook('morph.updated', ({ el }) => {
|
||||
if (el.id === 'logs') {
|
||||
this.$nextTick(() => {
|
||||
this.applySearch();
|
||||
if (this.alwaysScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// Re-render logs after Livewire updates (debounced)
|
||||
let renderTimeout = null;
|
||||
const debouncedRender = () => {
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(() => {
|
||||
this.matchCountCache = null; // Invalidate match cache on new content
|
||||
this.renderTrigger++;
|
||||
}, 100);
|
||||
};
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
this.$nextTick(debouncedRender);
|
||||
});
|
||||
Livewire.hook('commit', ({ succeed }) => {
|
||||
succeed(() => {
|
||||
this.$nextTick(debouncedRender);
|
||||
});
|
||||
});
|
||||
|
||||
// Stop auto-scroll when deployment finishes
|
||||
Livewire.on('deploymentFinished', () => {
|
||||
setTimeout(() => {
|
||||
this.stopScroll();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Start auto-scroll if deployment is in progress
|
||||
if (this.alwaysScroll) {
|
||||
this.scheduleScroll();
|
||||
|
|
@ -229,7 +187,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-co
|
|||
<span class="dark:text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<span x-show="searchQuery.trim()" x-text="getMatchCount() + ' matches'"
|
||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -324,7 +282,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
|
|||
class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar"
|
||||
:class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
|
||||
<div id="logs" class="flex flex-col font-mono">
|
||||
<div x-show="searchQuery.trim() && getMatchCount() === 0"
|
||||
<div x-show="searchQuery.trim() && matchCount === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
</div>
|
||||
|
|
@ -334,19 +292,19 @@ class="text-gray-500 dark:text-gray-400 py-2">
|
|||
$searchableContent = $line['timestamp'] . ' ' . $lineContent;
|
||||
@endphp
|
||||
<div data-log-line data-log-content="{{ htmlspecialchars($searchableContent) }}"
|
||||
x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))" @class([
|
||||
@class([
|
||||
'mt-2' => isset($line['command']) && $line['command'],
|
||||
'flex gap-2 log-line',
|
||||
])>
|
||||
<span x-show="showTimestamps"
|
||||
class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
|
||||
<span data-line-text="{{ htmlspecialchars($lineContent) }}" @class([
|
||||
'text-success dark:text-warning' => $line['hidden'],
|
||||
'text-red-500' => $line['stderr'],
|
||||
'font-bold' => isset($line['command']) && $line['command'],
|
||||
'whitespace-pre-wrap',
|
||||
])
|
||||
x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"></span>
|
||||
<span data-line-text="{{ htmlspecialchars($lineContent) }}"
|
||||
@class([
|
||||
'text-success dark:text-warning' => $line['hidden'],
|
||||
'text-red-500' => $line['stderr'],
|
||||
'font-bold' => isset($line['command']) && $line['command'],
|
||||
'whitespace-pre-wrap',
|
||||
])>{{ $lineContent }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<span class="font-mono text-neutral-400 mb-2">No logs yet.</span>
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@
|
|||
const range = selection.getRangeAt(0);
|
||||
return logsContainer.contains(range.commonAncestorContainer);
|
||||
},
|
||||
decodeHtml(text) {
|
||||
const doc = new DOMParser().parseFromString(text, 'text/html');
|
||||
return doc.documentElement.textContent;
|
||||
},
|
||||
applySearch() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return;
|
||||
|
|
@ -121,7 +125,7 @@
|
|||
|
||||
// Update highlighting
|
||||
if (textSpan) {
|
||||
const originalText = textSpan.dataset.lineText || '';
|
||||
const originalText = this.decodeHtml(textSpan.dataset.lineText || '');
|
||||
if (!query) {
|
||||
textSpan.textContent = originalText;
|
||||
} else if (matches) {
|
||||
|
|
@ -188,16 +192,28 @@
|
|||
this.applySearch();
|
||||
});
|
||||
|
||||
// Apply colors after Livewire updates
|
||||
// Handler for applying colors and search after DOM changes
|
||||
const applyAfterUpdate = () => {
|
||||
this.$nextTick(() => {
|
||||
this.applyColorLogs();
|
||||
this.applySearch();
|
||||
if (this.alwaysScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Apply colors after Livewire updates (existing content)
|
||||
Livewire.hook('morph.updated', ({ el }) => {
|
||||
if (el.id === 'logs') {
|
||||
this.$nextTick(() => {
|
||||
this.applyColorLogs();
|
||||
this.applySearch();
|
||||
if (this.alwaysScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
applyAfterUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
// Apply colors after Livewire adds new content (initial load)
|
||||
Livewire.hook('morph.added', ({ el }) => {
|
||||
if (el.id === 'logs') {
|
||||
applyAfterUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -375,7 +391,7 @@ class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0
|
|||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
</div>
|
||||
@foreach ($displayLines as $line)
|
||||
@foreach ($displayLines as $index => $line)
|
||||
@php
|
||||
// Parse timestamp from log line (ISO 8601 format: 2025-12-04T11:48:39.136764033Z)
|
||||
$timestamp = '';
|
||||
|
|
@ -392,11 +408,13 @@ class="text-gray-500 dark:text-gray-400 py-2">
|
|||
$monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
$monthName = $monthNames[(int)$month - 1] ?? $month;
|
||||
|
||||
// Format: 2025-Dec-04 09:44:58.198879
|
||||
$timestamp = "{$year}-{$monthName}-{$day} {$time}.{$microseconds}";
|
||||
// Format for display: 2025-Dec-04 09:44:58
|
||||
$timestamp = "{$year}-{$monthName}-{$day} {$time}";
|
||||
// Include microseconds in key for uniqueness
|
||||
$lineKey = "{$timestamp}.{$microseconds}";
|
||||
}
|
||||
@endphp
|
||||
<div data-log-line data-log-content="{{ $line }}" class="flex gap-2 log-line">
|
||||
<div wire:key="{{ $lineKey ?? 'line-' . $index }}" data-log-line data-log-content="{{ $line }}" class="flex gap-2 log-line">
|
||||
@if ($timestamp && $showTimeStamps)
|
||||
<span class="shrink-0 text-gray-500">{{ $timestamp }}</span>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ class="flex flex-col gap-1 p-2 border dark:border-coolgray-200 hover:no-underlin
|
|||
<div class="flex gap-2 pt-2">
|
||||
@can('view', $savedToken)
|
||||
<x-forms.button wire:click="validateToken({{ $savedToken->id }})" type="button">
|
||||
Validate Token
|
||||
Validate
|
||||
</x-forms.button>
|
||||
@endcan
|
||||
|
||||
@can('delete', $savedToken)
|
||||
<x-modal-confirmation title="Confirm Token Deletion?" isErrorButton buttonTitle="Delete Token"
|
||||
<x-modal-confirmation title="Confirm Token Deletion?" isErrorButton buttonTitle="Delete"
|
||||
submitAction="deleteToken({{ $savedToken->id }})" :actions="[
|
||||
'This cloud provider token will be permanently deleted.',
|
||||
'Any servers using this token will need to be reconfigured.',
|
||||
|
|
|
|||
110
resources/views/livewire/server/sentinel.blade.php
Normal file
110
resources/views/livewire/server/sentinel.blade.php
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Sentinel | Coolify
|
||||
</x-slot>
|
||||
<livewire:server.navbar :server="$server" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-server.sidebar :server="$server" activeMenu="sentinel" />
|
||||
<div class="w-full">
|
||||
<form wire:submit.prevent='submit'>
|
||||
<div class="flex gap-2 items-center pb-2">
|
||||
<h2>Sentinel</h2>
|
||||
<x-helper helper="Sentinel reports your server's & container's health and collects metrics." />
|
||||
@if ($server->isSentinelEnabled())
|
||||
<div class="flex gap-2 items-center">
|
||||
@if ($server->isSentinelLive())
|
||||
<x-status.running status="In sync" noLoading title="{{ $sentinelUpdatedAt }}" />
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$server">Save</x-forms.button>
|
||||
<x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server">Restart</x-forms.button>
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" displayName="Sentinel" :collapsible="false"
|
||||
lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@else
|
||||
<x-status.stopped status="Out of sync" noLoading
|
||||
title="{{ $sentinelUpdatedAt }}" />
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$server">Save</x-forms.button>
|
||||
<x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server">Sync</x-forms.button>
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" displayName="Sentinel" :collapsible="false"
|
||||
lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled"
|
||||
label="Enable Sentinel" />
|
||||
@if ($server->isSentinelEnabled())
|
||||
@if (isDev())
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" id="isSentinelDebugEnabled"
|
||||
label="Enable Sentinel (with debug)" instantSave />
|
||||
@endif
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||
id="isMetricsEnabled" label="Enable Metrics" />
|
||||
@else
|
||||
@if (isDev())
|
||||
<x-forms.checkbox id="isSentinelDebugEnabled" label="Enable Sentinel (with debug)"
|
||||
disabled instantSave />
|
||||
@endif
|
||||
<x-forms.checkbox instantSave disabled id="isMetricsEnabled"
|
||||
label="Enable Metrics (enable Sentinel first)" />
|
||||
@endif
|
||||
</div>
|
||||
@if (isDev() && $server->isSentinelEnabled())
|
||||
<div class="pt-4" x-data="{
|
||||
customImage: localStorage.getItem('sentinel_custom_docker_image_{{ $server->uuid }}') || '',
|
||||
saveCustomImage() {
|
||||
localStorage.setItem('sentinel_custom_docker_image_{{ $server->uuid }}', this.customImage);
|
||||
$wire.set('sentinelCustomDockerImage', this.customImage);
|
||||
}
|
||||
}" x-init="$wire.set('sentinelCustomDockerImage', customImage)">
|
||||
<x-forms.input x-model="customImage" @input.debounce.500ms="saveCustomImage()"
|
||||
placeholder="e.g., sentinel:latest or myregistry/sentinel:dev"
|
||||
label="Custom Sentinel Docker Image (Dev Only)"
|
||||
helper="Override the default Sentinel Docker image for testing. Leave empty to use the default." />
|
||||
</div>
|
||||
@endif
|
||||
@if ($server->isSentinelEnabled())
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap items-end">
|
||||
<x-forms.input canGate="update" :canResource="$server" type="password" id="sentinelToken"
|
||||
label="Sentinel token" required helper="Token for Sentinel." />
|
||||
<x-forms.button canGate="update" :canResource="$server"
|
||||
wire:click="regenerateSentinelToken">Regenerate</x-forms.button>
|
||||
</div>
|
||||
|
||||
<x-forms.input canGate="update" :canResource="$server" id="sentinelCustomUrl" required
|
||||
label="Coolify URL"
|
||||
helper="URL to your Coolify instance. If it is empty that means you do not have a FQDN set for your Coolify instance." />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap">
|
||||
<x-forms.input canGate="update" :canResource="$server"
|
||||
id="sentinelMetricsRefreshRateSeconds" label="Metrics rate (seconds)" required
|
||||
helper="Interval used for gathering metrics. Lower values result in more disk space usage." />
|
||||
<x-forms.input canGate="update" :canResource="$server" id="sentinelMetricsHistoryDays"
|
||||
label="Metrics history (days)" required
|
||||
helper="Number of days to retain metrics data for." />
|
||||
<x-forms.input canGate="update" :canResource="$server"
|
||||
id="sentinelPushIntervalSeconds" label="Push interval (seconds)" required
|
||||
helper="Interval at which metrics data is sent to the collector." />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -285,37 +285,6 @@ class="w-full input opacity-50 cursor-not-allowed"
|
|||
@endif
|
||||
</div>
|
||||
|
||||
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
|
||||
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span>
|
||||
</h3>
|
||||
<div class="pb-4">Read the docs <a class='underline dark:text-white'
|
||||
href='https://coolify.io/docs/knowledge-base/docker/swarm'
|
||||
target='_blank'>here</a>.
|
||||
</div>
|
||||
<div class="w-96">
|
||||
@if ($server->settings->is_swarm_worker)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox" id="isSwarmManager"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Manager?" />
|
||||
@else
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||
type="checkbox" id="isSwarmManager"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Manager?" :disabled="$isValidating" />
|
||||
@endif
|
||||
|
||||
@if ($server->settings->is_swarm_manager)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox" id="isSwarmWorker"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Worker?" />
|
||||
@else
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||
type="checkbox" id="isSwarmWorker"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Worker?" :disabled="$isValidating" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -337,6 +306,20 @@ class="w-full input opacity-50 cursor-not-allowed"
|
|||
@endforeach
|
||||
</x-forms.select>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<x-forms.input wire:model="manualHetznerServerId"
|
||||
label="Server ID"
|
||||
placeholder="e.g., 12345678"
|
||||
helper="Enter the Hetzner Server ID from your Hetzner Cloud console"
|
||||
canGate="update" :canResource="$server" />
|
||||
</div>
|
||||
<x-forms.button wire:click="searchHetznerServerById"
|
||||
wire:loading.attr="disabled"
|
||||
canGate="update" :canResource="$server">
|
||||
<span wire:loading.remove wire:target="searchHetznerServerById">Search by ID</span>
|
||||
<span wire:loading wire:target="searchHetznerServerById">Searching...</span>
|
||||
</x-forms.button>
|
||||
<div class="self-end pb-2 text-sm dark:text-neutral-500">OR</div>
|
||||
<x-forms.button wire:click="searchHetznerServer"
|
||||
wire:loading.attr="disabled"
|
||||
canGate="update" :canResource="$server">
|
||||
|
|
@ -354,10 +337,14 @@ class="w-full input opacity-50 cursor-not-allowed"
|
|||
@if ($hetznerNoMatchFound)
|
||||
<div class="mt-4 p-4 border border-yellow-500 rounded-md bg-yellow-50 dark:bg-yellow-900/20">
|
||||
<p class="text-yellow-600 dark:text-yellow-400">
|
||||
No Hetzner server found matching IP: {{ $server->ip }}
|
||||
@if ($manualHetznerServerId)
|
||||
No Hetzner server found with ID: {{ $manualHetznerServerId }}
|
||||
@else
|
||||
No Hetzner server found matching IP: {{ $server->ip }}
|
||||
@endif
|
||||
</p>
|
||||
<p class="text-sm dark:text-neutral-400 mt-1">
|
||||
Try a different token or verify the server IP is correct.
|
||||
Try a different token, enter the Server ID manually, or verify the details are correct.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -378,116 +365,6 @@ class="w-full input opacity-50 cursor-not-allowed"
|
|||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer())
|
||||
<form wire:submit.prevent='submit'>
|
||||
<div class="flex gap-2 items-center pt-4 pb-2">
|
||||
<h3>Sentinel</h3>
|
||||
<x-helper helper="Sentinel reports your server's & container's health and collects metrics." />
|
||||
@if ($server->isSentinelEnabled())
|
||||
<div class="flex gap-2 items-center">
|
||||
@if ($server->isSentinelLive())
|
||||
<x-status.running status="In sync" noLoading title="{{ $sentinelUpdatedAt }}" />
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$server"
|
||||
:disabled="$isValidating">Save</x-forms.button>
|
||||
<x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server"
|
||||
:disabled="$isValidating">Restart</x-forms.button>
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" displayName="Sentinel" :collapsible="false"
|
||||
lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true"
|
||||
:disabled="$isValidating">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@else
|
||||
<x-status.stopped status="Out of sync" noLoading
|
||||
title="{{ $sentinelUpdatedAt }}" />
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$server"
|
||||
:disabled="$isValidating">Save</x-forms.button>
|
||||
<x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server"
|
||||
:disabled="$isValidating">Sync</x-forms.button>
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" displayName="Sentinel" :collapsible="false"
|
||||
lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true"
|
||||
:disabled="$isValidating">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled"
|
||||
label="Enable Sentinel" :disabled="$isValidating" />
|
||||
@if ($server->isSentinelEnabled())
|
||||
@if (isDev())
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" id="isSentinelDebugEnabled"
|
||||
label="Enable Sentinel (with debug)" instantSave :disabled="$isValidating" />
|
||||
@endif
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||
id="isMetricsEnabled" label="Enable Metrics" :disabled="$isValidating" />
|
||||
@else
|
||||
@if (isDev())
|
||||
<x-forms.checkbox id="isSentinelDebugEnabled" label="Enable Sentinel (with debug)"
|
||||
disabled instantSave />
|
||||
@endif
|
||||
<x-forms.checkbox instantSave disabled id="isMetricsEnabled"
|
||||
label="Enable Metrics (enable Sentinel first)" />
|
||||
@endif
|
||||
</div>
|
||||
@if (isDev() && $server->isSentinelEnabled())
|
||||
<div class="pt-4" x-data="{
|
||||
customImage: localStorage.getItem('sentinel_custom_docker_image_{{ $server->uuid }}') || '',
|
||||
saveCustomImage() {
|
||||
localStorage.setItem('sentinel_custom_docker_image_{{ $server->uuid }}', this.customImage);
|
||||
$wire.set('sentinelCustomDockerImage', this.customImage);
|
||||
}
|
||||
}" x-init="$wire.set('sentinelCustomDockerImage', customImage)">
|
||||
<x-forms.input x-model="customImage" @input.debounce.500ms="saveCustomImage()"
|
||||
placeholder="e.g., sentinel:latest or myregistry/sentinel:dev"
|
||||
label="Custom Sentinel Docker Image (Dev Only)"
|
||||
helper="Override the default Sentinel Docker image for testing. Leave empty to use the default." />
|
||||
</div>
|
||||
@endif
|
||||
@if ($server->isSentinelEnabled())
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap items-end">
|
||||
<x-forms.input canGate="update" :canResource="$server" type="password" id="sentinelToken"
|
||||
label="Sentinel token" required helper="Token for Sentinel." :disabled="$isValidating" />
|
||||
<x-forms.button canGate="update" :canResource="$server"
|
||||
wire:click="regenerateSentinelToken" :disabled="$isValidating">Regenerate</x-forms.button>
|
||||
</div>
|
||||
|
||||
<x-forms.input canGate="update" :canResource="$server" id="sentinelCustomUrl" required
|
||||
label="Coolify URL"
|
||||
helper="URL to your Coolify instance. If it is empty that means you do not have a FQDN set for your Coolify instance."
|
||||
:disabled="$isValidating" />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap">
|
||||
<x-forms.input canGate="update" :canResource="$server"
|
||||
id="sentinelMetricsRefreshRateSeconds" label="Metrics rate (seconds)" required
|
||||
helper="Interval used for gathering metrics. Lower values result in more disk space usage."
|
||||
:disabled="$isValidating" />
|
||||
<x-forms.input canGate="update" :canResource="$server" id="sentinelMetricsHistoryDays"
|
||||
label="Metrics history (days)" required
|
||||
helper="Number of days to retain metrics data for." :disabled="$isValidating" />
|
||||
<x-forms.input canGate="update" :canResource="$server"
|
||||
id="sentinelPushIntervalSeconds" label="Push interval (seconds)" required
|
||||
helper="Interval at which metrics data is sent to the collector."
|
||||
:disabled="$isValidating" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
43
resources/views/livewire/server/swarm.blade.php
Normal file
43
resources/views/livewire/server/swarm.blade.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Swarm | Coolify
|
||||
</x-slot>
|
||||
<livewire:server.navbar :server="$server" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-server.sidebar :server="$server" activeMenu="swarm" />
|
||||
<div class="w-full">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Swarm <span class="text-xs text-neutral-500">(experimental)</span></h2>
|
||||
</div>
|
||||
<div class="pb-4">Read the docs <a class='underline dark:text-white'
|
||||
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-96">
|
||||
@if ($server->settings->is_swarm_worker)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox" id="isSwarmManager"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Manager?" />
|
||||
@else
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||
type="checkbox" id="isSwarmManager"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Manager?" />
|
||||
@endif
|
||||
|
||||
@if ($server->settings->is_swarm_manager)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox" id="isSwarmWorker"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Worker?" />
|
||||
@else
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||
type="checkbox" id="isSwarmWorker"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Worker?" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -56,7 +56,9 @@
|
|||
use App\Livewire\Server\Resources as ResourcesShow;
|
||||
use App\Livewire\Server\Security\Patches;
|
||||
use App\Livewire\Server\Security\TerminalAccess;
|
||||
use App\Livewire\Server\Sentinel as ServerSentinel;
|
||||
use App\Livewire\Server\Show as ServerShow;
|
||||
use App\Livewire\Server\Swarm as ServerSwarm;
|
||||
use App\Livewire\Settings\Advanced as SettingsAdvanced;
|
||||
use App\Livewire\Settings\Index as SettingsIndex;
|
||||
use App\Livewire\Settings\Updates as SettingsUpdates;
|
||||
|
|
@ -251,6 +253,8 @@
|
|||
Route::prefix('server/{server_uuid}')->group(function () {
|
||||
Route::get('/', ServerShow::class)->name('server.show');
|
||||
Route::get('/advanced', ServerAdvanced::class)->name('server.advanced');
|
||||
Route::get('/swarm', ServerSwarm::class)->name('server.swarm');
|
||||
Route::get('/sentinel', ServerSentinel::class)->name('server.sentinel');
|
||||
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');
|
||||
Route::get('/cloud-provider-token', CloudProviderTokenShow::class)->name('server.cloud-provider-token');
|
||||
Route::get('/ca-certificate', CaCertificateShow::class)->name('server.ca-certificate');
|
||||
|
|
|
|||
|
|
@ -4350,7 +4350,7 @@
|
|||
"umami": {
|
||||
"documentation": "https://umami.is?utm_source=coolify.io",
|
||||
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
|
||||
"tags": [
|
||||
"analytics",
|
||||
"insights",
|
||||
|
|
|
|||
|
|
@ -4350,7 +4350,7 @@
|
|||
"umami": {
|
||||
"documentation": "https://umami.is?utm_source=coolify.io",
|
||||
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
||||
"tags": [
|
||||
"analytics",
|
||||
"insights",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.456"
|
||||
"version": "4.0.0-beta.457"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.457"
|
||||
"version": "4.0.0-beta.458"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.12"
|
||||
|
|
|
|||
Loading…
Reference in a new issue