Merge branch 'coollabsio:main' into improve-release.md

This commit is contained in:
peaklabs-dev
2024-09-18 20:44:24 +02:00
committed by GitHub
28 changed files with 241 additions and 187 deletions

View File

@@ -0,0 +1,75 @@
name: Remove Labels and Assignees on Issue Close
on:
issues:
types: [closed]
pull_request:
types: [closed]
pull_request_target:
types: [closed]
jobs:
remove-labels-and-assignees:
runs-on: ubuntu-latest
steps:
- name: Remove labels and assignees
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
async function processIssue(issueNumber) {
try {
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: issueNumber
});
const labelsToKeep = currentLabels
.filter(label => label.name === '⏱︎ Stale')
.map(label => label.name);
await github.rest.issues.setLabels({
owner,
repo,
issue_number: issueNumber,
labels: labelsToKeep
});
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});
if (issue.assignees && issue.assignees.length > 0) {
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: issue.assignees.map(assignee => assignee.login)
});
}
} catch (error) {
if (error.status !== 404) {
console.error(`Error processing issue ${issueNumber}:`, error);
}
}
}
if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const issue = context.payload.issue || context.payload.pull_request;
await processIssue(issue.number);
}
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const { data: closedIssues } = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:issue is:closed linked:${context.payload.pull_request.number}`,
per_page: 100
});
for (const issue of closedIssues.items) {
await processIssue(issue.number);
}
}

View File

@@ -42,8 +42,8 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
$current_version = $settings->helper_version; $current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) { if (version_compare($latest_version, $current_version, '>')) {
// New version available // New version available
$helperImage = config('coolify.helper_image'); // $helperImage = config('coolify.helper_image');
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); // instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
$settings->update(['helper_version' => $latest_version]); $settings->update(['helper_version' => $latest_version]);
} }
} }

View File

@@ -20,6 +20,8 @@ class Navbar extends Component
public $isDeploymentProgress = false; public $isDeploymentProgress = false;
public $title = 'Configuration';
public function mount() public function mount()
{ {
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {

View File

@@ -33,6 +33,9 @@ class ExecuteContainerCommand extends Component
public function mount() public function mount()
{ {
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->containers = collect(); $this->containers = collect();
$this->servers = collect(); $this->servers = collect();
@@ -130,7 +133,6 @@ class ExecuteContainerCommand extends Component
{ {
try { try {
$container_name = data_get($this->container, 'container.Names'); $container_name = data_get($this->container, 'container.Names');
ray($this->container);
if (is_null($container_name)) { if (is_null($container_name)) {
throw new \RuntimeException('Container not found.'); throw new \RuntimeException('Container not found.');
} }

View File

@@ -14,13 +14,6 @@ class Terminal extends Component
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
// if (auth()->user()) {
// $teams = auth()->user()->teams->pluck('id');
// if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
// throw new \Exception('User is not part of the team that owns this server');
// }
// }
if ($isContainer) { if ($isContainer) {
$status = getContainerStatus($server, $identifier); $status = getContainerStatus($server, $identifier);
if ($status !== 'running') { if ($status !== 'running') {

View File

@@ -1,101 +0,0 @@
<?php
namespace App\Livewire;
use Livewire\Attributes\On;
use Livewire\Component;
class RunCommand extends Component
{
public $selected_uuid = 'default';
public $servers = [];
public $containers = [];
public function mount($servers)
{
$this->servers = $servers;
$this->containers = $this->getAllActiveContainers();
}
private function getAllActiveContainers()
{
return collect($this->servers)->flatMap(function ($server) {
if (! $server->isFunctional()) {
return [];
}
return $server->definedResources()
->filter(function ($resource) {
$status = method_exists($resource, 'realStatus') ? $resource->realStatus() : (method_exists($resource, 'status') ? $resource->status() : 'exited');
return str_starts_with($status, 'running:');
})
->map(function ($resource) use ($server) {
if (isDev()) {
if (data_get($resource, 'name') === 'coolify-db') {
$container_name = 'coolify-db';
return [
'name' => $resource->name,
'connection_name' => $container_name,
'uuid' => $resource->uuid,
'status' => 'running',
'server' => $server,
'server_uuid' => $server->uuid,
];
}
}
if (class_basename($resource) === 'Application') {
if (! $server->isSwarm()) {
$current_containers = getCurrentApplicationContainerStatus($server, $resource->id, includePullrequests: true);
}
$status = $resource->status;
} elseif (class_basename($resource) === 'Service') {
$current_containers = getCurrentServiceContainerStatus($server, $resource->id);
$status = $resource->status();
} else {
$status = getContainerStatus($server, $resource->uuid);
if ($status === 'running') {
$current_containers = collect([
'Names' => $resource->name,
]);
}
}
if ($server->isSwarm()) {
$container_name = $resource->uuid.'_'.$resource->uuid;
} else {
$container_name = data_get($current_containers->first(), 'Names');
}
return [
'name' => $resource->name,
'connection_name' => $container_name,
'uuid' => $resource->uuid,
'status' => $status,
'server' => $server,
'server_uuid' => $server->uuid,
];
});
});
}
public function updatedSelectedUuid($value)
{
$this->connectToContainer();
}
#[On('connectToContainer')]
public function connectToContainer()
{
$container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
$this->dispatch('send-terminal-command',
isset($container),
$container['connection_name'] ?? $this->selected_uuid,
$container['server_uuid'] ?? $this->selected_uuid
);
}
}

View File

@@ -3,15 +3,70 @@
namespace App\Livewire\Terminal; namespace App\Livewire\Terminal;
use App\Models\Server; use App\Models\Server;
use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
class Index extends Component class Index extends Component
{ {
public $selected_uuid = 'default';
public $servers = []; public $servers = [];
public $containers = [];
public function mount() public function mount()
{ {
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->servers = Server::isReachable()->get(); $this->servers = Server::isReachable()->get();
$this->containers = $this->getAllActiveContainers();
}
private function getAllActiveContainers()
{
return collect($this->servers)->flatMap(function ($server) {
if (! $server->isFunctional()) {
return [];
}
return $server->loadAllContainers()->map(function ($container) use ($server) {
$state = data_get_str($container, 'State')->lower();
if ($state->contains('running')) {
return [
'name' => data_get($container, 'Names'),
'connection_name' => data_get($container, 'Names'),
'uuid' => data_get($container, 'Names'),
'status' => data_get_str($container, 'State')->lower(),
'server' => $server,
'server_uuid' => $server->uuid,
];
}
return null;
})->filter();
});
}
public function updatedSelectedUuid()
{
$this->connectToContainer();
}
#[On('connectToContainer')]
public function connectToContainer()
{
if ($this->selected_uuid === 'default') {
$this->dispatch('error', 'Please select a server or a container.');
return;
}
$container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
$this->dispatch('send-terminal-command',
isset($container),
$container['connection_name'] ?? $this->selected_uuid,
$container['server_uuid'] ?? $this->selected_uuid
);
} }
public function render() public function render()

View File

@@ -413,7 +413,7 @@ $schema://$host {
handle /app/* { handle /app/* {
reverse_proxy coolify-realtime:6001 reverse_proxy coolify-realtime:6001
} }
handle /terminal/ws/* { handle /terminal/ws {
reverse_proxy coolify-realtime:6002 reverse_proxy coolify-realtime:6002
} }
reverse_proxy coolify:80 reverse_proxy coolify:80
@@ -775,6 +775,18 @@ $schema://$host {
} }
} }
public function loadAllContainers(): Collection
{
if ($this->isFunctional()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);
$containers = format_docker_command_output_to_json($containers);
return collect($containers);
}
return collect([]);
}
public function loadUnmanagedContainers(): Collection public function loadUnmanagedContainers(): Collection
{ {
if ($this->isFunctional()) { if ($this->isFunctional()) {

View File

@@ -2985,7 +2985,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
// Get magic environments where we need to preset the FQDN // Get magic environments where we need to preset the FQDN
if ($key->startsWith('SERVICE_FQDN_')) { if ($key->startsWith('SERVICE_FQDN_')) {
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
if (substr_count(str($key)->value(), '_') === 3) {
$fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
} else {
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
}
if ($isApplication) { if ($isApplication) {
$fqdn = generateFqdn($server, "{$resource->name}-$uuid"); $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
} elseif ($isService) { } elseif ($isService) {

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.337', 'release' => '4.0.0-beta.341',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.337'; return '4.0.0-beta.341';

View File

@@ -46,6 +46,9 @@ services:
- PUSHER_APP_ID - PUSHER_APP_ID
- PUSHER_APP_KEY - PUSHER_APP_KEY
- PUSHER_APP_SECRET - PUSHER_APP_SECRET
- TERMINAL_PROTOCOL
- TERMINAL_HOST
- TERMINAL_PORT
- AUTOUPDATE - AUTOUPDATE
- SELF_HOSTED - SELF_HOSTED
- SSH_MUX_ENABLED - SSH_MUX_ENABLED
@@ -110,7 +113,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -123,7 +123,6 @@ async function handleCommand(ws, command, userId) {
cols: 80, cols: 80,
rows: 30, rows: 30,
cwd: process.env.HOME, cwd: process.env.HOME,
env: process.env
}; };
// NOTE: - Initiates a process within the Terminal container // NOTE: - Initiates a process within the Terminal container

View File

@@ -110,7 +110,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.337" "version": "4.0.0-beta.339"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.338" "version": "4.0.0-beta.340"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"
}, },
"realtime": { "realtime": {
"version": "1.0.0" "version": "1.0.1"
} }
} }
} }

8
package-lock.json generated
View File

@@ -26,7 +26,7 @@
"postcss": "8.4.38", "postcss": "8.4.38",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.4", "tailwindcss": "3.4.4",
"vite": "4.5.3", "vite": "4.5.5",
"vue": "3.4.29" "vue": "3.4.29"
} }
}, },
@@ -2082,9 +2082,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.3", "version": "4.5.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",

View File

@@ -14,7 +14,7 @@
"postcss": "8.4.38", "postcss": "8.4.38",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.4", "tailwindcss": "3.4.4",
"vite": "4.5.3", "vite": "4.5.5",
"vue": "3.4.29" "vue": "3.4.29"
}, },
"dependencies": { "dependencies": {

BIN
public/svgs/budge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -23,10 +23,6 @@
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'" @click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks href="#">Scheduled Tasks
</a> </a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'terminal' && 'menu-item-active'"
@click.prevent="activeTab = 'terminal';
window.location.hash = 'terminal'"
href="#">Terminal</a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'logs' && 'menu-item-active'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'logs' && 'menu-item-active'"
@click.prevent="activeTab = 'logs'; @click.prevent="activeTab = 'logs';
window.location.hash = 'logs'" window.location.hash = 'logs'"
@@ -191,9 +187,6 @@
<div x-cloak x-show="activeTab === 'logs'"> <div x-cloak x-show="activeTab === 'logs'">
<livewire:project.shared.logs :resource="$service" /> <livewire:project.shared.logs :resource="$service" />
</div> </div>
<div x-cloak x-show="activeTab === 'terminal'">
<livewire:project.shared.execute-container-command :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$service" /> <livewire:project.shared.environment-variable.all :resource="$service" />
</div> </div>

View File

@@ -6,17 +6,21 @@
<livewire:activity-monitor header="Logs" showWaiting fullHeight /> <livewire:activity-monitor header="Logs" showWaiting fullHeight />
</x-slot:content> </x-slot:content>
</x-slide-over> </x-slide-over>
<h1>Configuration</h1> <h1>{{ $title }}</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data> <div class="navbar-main" x-data>
<nav class="flex items-center flex-shrink-0 gap-6 scrollbar min-h-10 whitespace-nowrap"> <nav class="flex flex-shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}"
href="{{ route('project.service.configuration', $parameters) }}"> href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<a class="{{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.service.command', $parameters) }}">
<button>Terminal</button>
</a>
<x-services.links :service="$service" /> <x-services.links :service="$service" />
</nav> </nav>
<div class="flex flex-wrap items-center order-first gap-2 sm:order-last"> <div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
@if (str($service->status())->contains('running')) @if (str($service->status())->contains('running'))
<button @click="$wire.dispatch('restartEvent')" class="gap-2 button"> <button @click="$wire.dispatch('restartEvent')" class="gap-2 button">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -10,7 +10,7 @@
<h1>Terminal</h1> <h1>Terminal</h1>
<livewire:project.database.heading :database="$resource" /> <livewire:project.database.heading :database="$resource" />
@elseif ($type === 'service') @elseif ($type === 'service')
<h2>Terminal</h2> <livewire:project.service.navbar :service="$resource" :parameters="$parameters" title="Terminal" />
@endif @endif
<div x-init="$wire.loadContainers"> <div x-init="$wire.loadContainers">
<div class="pt-4" wire:loading wire:target='loadContainers'> <div class="pt-4" wire:loading wire:target='loadContainers'>

View File

@@ -50,23 +50,42 @@
function initializeWebSocket() { function initializeWebSocket() {
if (!socket || socket.readyState === WebSocket.CLOSED) { if (!socket || socket.readyState === WebSocket.CLOSED) {
// Only use port if Coolify is used with ip (so it has a port in the url) const predefined = {
let postPath = ':6002/terminal/ws'; protocol: "{{ env('TERMINAL_PROTOCOL') }}",
const port = window.location.port; host: "{{ env('TERMINAL_HOST') }}",
if (!port) { port: "{{ env('TERMINAL_PORT') }}"
postPath = '/terminal/ws';
} }
let url = window.location.hostname; const connectionString = {
// make sure the port is not included protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
url = url.split(':')[0]; host: window.location.hostname,
socket = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') + port: ":6002",
url + path: '/terminal/ws'
postPath); }
if (!window.location.port) {
connectionString.port = ''
}
if (predefined.host) {
connectionString.host = predefined.host
}
if (predefined.port) {
connectionString.port = `:${predefined.port}`
}
if (predefined.protocol) {
connectionString.protocol = predefined.protocol
}
const url =
`${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
socket = new WebSocket(url);
socket.onmessage = handleSocketMessage; socket.onmessage = handleSocketMessage;
socket.onerror = (e) => { socket.onerror = (e) => {
console.error('WebSocket error:', e); console.error('WebSocket error:', e);
}; };
socket.onclose = () => {
console.log('WebSocket connection closed');
};
} }
} }
@@ -112,9 +131,8 @@
socket.send(JSON.stringify({ socket.send(JSON.stringify({
message: data message: data
})); }));
// Type CTRL + D or exit in the terminal // Type CTRL + D or exit in the terminal
if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim() === 'exit')) { if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim().includes('exit'))) {
checkIfProcessIsRunningAndKillIt(); checkIfProcessIsRunningAndKillIt();
setTimeout(() => { setTimeout(() => {
$data.terminalActive = false; $data.terminalActive = false;

View File

@@ -1,22 +0,0 @@
<div>
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select id="server" required wire:model.live="selected_uuid">
@foreach ($servers as $server)
@if ($loop->first)
<option disabled value="default">Select a server or container</option>
@endif
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
@foreach ($containers as $container)
@if ($container['server_uuid'] == $server->uuid)
<option value="{{ $container['uuid'] }}">
{{ $server->name }} -> {{ $container['name'] }}
</option>
@endif
@endforeach
@endforeach
</x-forms.select>
<x-forms.button type="submit">Connect</x-forms.button>
</form>
<livewire:project.shared.terminal />
</div>

View File

@@ -8,11 +8,27 @@
<x-helper <x-helper
helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper> helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper>
</div> </div>
@if ($servers->count() > 0)
<livewire:run-command :servers="$servers" />
@else
<div> <div>
<div>No servers found. Without a server, you won't be able to do much.</div> <form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
</div> wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select id="server" required wire:model.live="selected_uuid">
@foreach ($servers as $server)
@if ($loop->first)
<option disabled value="default">Select a server or container</option>
@endif @endif
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
@foreach ($containers as $container)
@if ($container['server_uuid'] == $server->uuid)
<option value="{{ $container['uuid'] }}">
{{ $server->name }} -> {{ $container['name'] }}
</option>
@endif
@endforeach
@endforeach
</x-forms.select>
<x-forms.button type="submit">Connect</x-forms.button>
</form>
<livewire:project.shared.terminal />
</div>
</div> </div>

View File

@@ -195,8 +195,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
}); });
Route::prefix('project/{project_uuid}/{environment_name}/service/{service_uuid}')->group(function () { Route::prefix('project/{project_uuid}/{environment_name}/service/{service_uuid}')->group(function () {
Route::get('/', ServiceConfiguration::class)->name('project.service.configuration'); Route::get('/', ServiceConfiguration::class)->name('project.service.configuration');
Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index');
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.service.command'); Route::get('/terminal', ExecuteContainerCommand::class)->name('project.service.command');
Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index');
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks'); Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks');
}); });

View File

@@ -1,6 +1,7 @@
# documentation: https://github.com/linuxserver/budge # documentation: https://github.com/linuxserver/budge
# slogan: A budgeting personal finance app. # slogan: A budgeting personal finance app.
# tags: personal finance, budgeting, expense tracking # tags: personal finance, budgeting, expense tracking
# logo: svgs/budge.png
services: services:
budge: budge:

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.337" "version": "4.0.0-beta.341"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.338" "version": "4.0.0-beta.342"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"
}, },
"realtime": { "realtime": {
"version": "1.0.0" "version": "1.0.1"
} }
} }
} }