caddy lables
This commit is contained in:
+29
-41
@@ -54,78 +54,66 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-4">
|
<div class="w-full mt-4 block clear-both">
|
||||||
<div class="p-2.5 bg-gray-950/40 rounded-lg border border-gray-850 flex flex-col justify-center min-h-[52px]">
|
<div class="p-3 bg-gray-900/30 rounded-lg border border-gray-800 flex flex-col justify-center min-h-[64px]">
|
||||||
|
|
||||||
<div *ngIf="!editingDomains[container.id]" class="flex items-center justify-between gap-2 w-full">
|
<div *ngIf="!editingDomains[container.id]" class="flex items-center justify-between gap-4 w-full">
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<span class="text-[10px] uppercase tracking-wider font-bold text-gray-500">Маршрутизация сети:</span>
|
<span class="text-[10px] uppercase tracking-wider font-bold text-gray-500">Маршрутизация сети:</span>
|
||||||
|
|
||||||
<ng-container *ngIf="getContainerDomain(container.name) as domain; else noDomain">
|
<ng-container *ngIf="getContainerDomain(container.name) as domain; else noDomain">
|
||||||
<div class="flex items-center gap-1.5 mt-0.5">
|
<div class="flex items-center gap-2 mt-1">
|
||||||
<a [href]="'https://' + domain" target="_blank" class="text-xs font-semibold text-blue-400 hover:text-blue-300 hover:underline flex items-center gap-1 font-mono">
|
<a [href]="'https://' + domain" target="_blank" class="text-xs font-semibold text-blue-400 hover:text-blue-300 hover:underline flex items-center gap-1 font-mono">
|
||||||
<svg class="w-3 h-3 text-blue-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<mat-icon class="!w-3.5 !h-3.5 !text-sm text-blue-400 shrink-0">open_in_new</mat-icon>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h14a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
<span class="truncate max-w-[200px]">{{ domain }}</span>
|
||||||
</svg>
|
|
||||||
<span class="truncate max-w-[180px]">{{ domain }}</span>
|
|
||||||
</a>
|
</a>
|
||||||
<span class="text-[10px] text-gray-500 font-mono bg-gray-900 px-1.5 py-0.25 rounded border border-gray-800 shrink-0">
|
<span class="text-[10px] text-gray-400 font-mono bg-gray-950 px-2 py-0.5 rounded border border-gray-850 shrink-0">
|
||||||
:{{ getContainerPort(container.name) }}
|
порт {{ getContainerPort(container.name) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #noDomain>
|
<ng-template #noDomain>
|
||||||
<span class="text-xs text-gray-600 italic mt-0.5">Локальный стек (нет публичного прокси)</span>
|
<span class="text-xs text-gray-600 italic mt-1">Локальный стек (нет публичного прокси)</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button (click)="startEditDomain(container)" class="p-1 text-gray-500 hover:text-blue-400 hover:bg-gray-900 rounded border border-transparent hover:border-gray-850 transition shadow-sm shrink-0" title="Изменить настройки Caddy">
|
<button mat-icon-button (click)="startEditDomain(container)" class="!w-8 !h-8 text-gray-500 hover:text-blue-400 transition shrink-0" title="Изменить настройки Caddy">
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<mat-icon class="!text-lg">edit</mat-icon>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="editingDomains[container.id]" class="w-full space-y-2 animate-fadeIn">
|
<div *ngIf="editingDomains[container.id]" class="w-full flex flex-col gap-2 pt-1">
|
||||||
<div class="flex items-center justify-between border-b border-gray-850 pb-1 mb-1">
|
<div class="flex items-center justify-between border-b border-gray-800 pb-2 mb-1">
|
||||||
<span class="text-[9px] uppercase tracking-wider font-bold text-blue-400">Настройка шлюза</span>
|
<span class="text-[10px] uppercase tracking-wider font-bold text-blue-400 flex items-center gap-1">
|
||||||
<div class="flex gap-1.5">
|
<mat-icon class="!w-3.5 !h-3.5 !text-sm">router</mat-icon> Настройка шлюза шлюза
|
||||||
<button (click)="saveDomainConfig(container)" class="bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-bold px-2 py-0.5 rounded transition shadow-md">
|
</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button mat-flat-button color="primary" (click)="saveDomainConfig(container)" class="!min-w-0 !px-3 !h-7 !line-height-[28px] !text-xs font-bold">
|
||||||
OK
|
OK
|
||||||
</button>
|
</button>
|
||||||
<button (click)="cancelEditDomain(container.id)" class="bg-gray-800 hover:bg-gray-750 text-gray-400 text-[10px] font-medium px-2 py-0.5 rounded transition">
|
<button mat-button (click)="cancelEditDomain(container.id)" class="!min-w-0 !px-3 !h-7 !line-height-[28px] !text-xs !text-gray-400">
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-1.5">
|
<div class="flex gap-3 w-full pt-1">
|
||||||
<div class="col-span-2 relative flex items-center">
|
<mat-form-field appearance="outline" class="flex-[2] !text-xs font-mono" subscriptSizing="dynamic">
|
||||||
<input
|
<mat-label>Поддомен</mat-label>
|
||||||
type="text"
|
<input matInput type="text" [(ngModel)]="tempSubdomain" placeholder="example" class="lowercase" />
|
||||||
[(ngModel)]="tempSubdomain"
|
<span matSuffix class="text-gray-600 pr-1 text-[11px] font-mono pointer-events-none">.jambotron.com</span>
|
||||||
placeholder="subdomain"
|
</mat-form-field>
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded pl-2 pr-12 py-1 text-xs text-gray-200 font-mono focus:outline-none focus:border-blue-600 lowercase"
|
|
||||||
/>
|
|
||||||
<span class="absolute right-2 text-[9px] font-mono text-gray-600 pointer-events-none">.jamb</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<mat-form-field appearance="outline" class="flex-1 !text-xs font-mono" subscriptSizing="dynamic">
|
||||||
<input
|
<mat-label>Порт</mat-label>
|
||||||
type="number"
|
<input matInput type="number" [(ngModel)]="tempPort" placeholder="80" min="1" max="65535" />
|
||||||
[(ngModel)]="tempPort"
|
</mat-form-field>
|
||||||
placeholder="Порт"
|
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded px-2 py-1 text-xs text-gray-200 font-mono focus:outline-none focus:border-blue-600"
|
|
||||||
title="Внутренний порт службы внутри docker-compose"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -104,3 +104,19 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-mdc-form-field {
|
||||||
|
width: 100%;
|
||||||
|
--mdc-outlined-text-field-container-shape: 6px;
|
||||||
|
--mdc-outlined-text-field-input-text-color: #e5e7eb;
|
||||||
|
--mdc-outlined-text-field-focus-input-text-color: #ffffff;
|
||||||
|
--mdc-outlined-text-field-label-text-color: #6b7280;
|
||||||
|
--mdc-outlined-text-field-focus-label-text-color: #3b82f6;
|
||||||
|
--mdc-outlined-text-field-outline-color: #1f2937;
|
||||||
|
--mdc-outlined-text-field-focus-outline-color: #2563eb;
|
||||||
|
|
||||||
|
.mat-mdc-text-field-wrapper {
|
||||||
|
background-color: rgba(3, 7, 18, 0.6) !important; // Глубокий тёмный фон bg-gray-950
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-19
@@ -16,6 +16,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|||||||
import { MatDividerModule } from '@angular/material/divider';
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
import { MatCard, MatCardContent } from '@angular/material/card';
|
import { MatCard, MatCardContent } from '@angular/material/card';
|
||||||
import {FormsModule} from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-project-details',
|
selector: 'app-project-details',
|
||||||
@@ -28,7 +30,9 @@ import {FormsModule} from '@angular/forms';
|
|||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
MatCard,
|
MatCard,
|
||||||
MatCardContent
|
MatCardContent,
|
||||||
|
MatFormFieldModule, // <-- Включаем в сборку созидания
|
||||||
|
MatInputModule // <-- Включаем в сборку созидания
|
||||||
],
|
],
|
||||||
templateUrl: './project-details.component.html',
|
templateUrl: './project-details.component.html',
|
||||||
styleUrls: ['./project-details.component.scss']
|
styleUrls: ['./project-details.component.scss']
|
||||||
@@ -252,19 +256,18 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
// Хранит состояние редактирования для каждого контейнера: { [containerId]: boolean }
|
// Хранит состояние редактирования для каждого контейнера: { [containerId]: boolean }
|
||||||
editingDomains: { [key: string]: boolean } = {};
|
editingDomains: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
// Временные переменные для хранения ввода пользователя во время редактирования
|
// Временные переменные для хранения ввода пользователя во время инлайн-редактирования
|
||||||
tempSubdomain: string = '';
|
tempSubdomain: string = '';
|
||||||
tempPort: number = 80;
|
tempPort: number = 80;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Извлекает имя сервиса (например, 'web') из полного имени Docker-контейнера
|
* Безопасно извлекает имя сервиса (например, 'web') из имени Docker-контейнера.
|
||||||
|
* Проверяет префиксы как по ID, так и по Name проекта.
|
||||||
*/
|
*/
|
||||||
getServiceName(containerName: string): string | null {
|
getServiceName(containerName: string): string | null {
|
||||||
if (!containerName || !this.project?.id) return null;
|
if (!containerName || !this.project) return null;
|
||||||
|
|
||||||
// Проверяем префикс по ID проекта
|
|
||||||
const prefixId = `jambotron-${this.project.id}-`;
|
const prefixId = `jambotron-${this.project.id}-`;
|
||||||
// Резервный префикс по имени проекта (на случай, если бэкенд использует его)
|
|
||||||
const prefixName = `jambotron-${this.project.name}-`;
|
const prefixName = `jambotron-${this.project.name}-`;
|
||||||
|
|
||||||
let servicePart = '';
|
let servicePart = '';
|
||||||
@@ -274,7 +277,7 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
} else if (containerName.startsWith(prefixName)) {
|
} else if (containerName.startsWith(prefixName)) {
|
||||||
servicePart = containerName.substring(prefixName.length);
|
servicePart = containerName.substring(prefixName.length);
|
||||||
} else {
|
} else {
|
||||||
// Фолбэк-разбор для коротких имён, если префиксы не подошли
|
// Резервный разбор по токенам (jambotron-проект-сервис)
|
||||||
const parts = containerName.split('-');
|
const parts = containerName.split('-');
|
||||||
if (parts.length >= 3 && parts[0] === 'jambotron') {
|
if (parts.length >= 3 && parts[0] === 'jambotron') {
|
||||||
servicePart = parts.slice(2).join('-');
|
servicePart = parts.slice(2).join('-');
|
||||||
@@ -283,7 +286,7 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отрезаем индекс реплики Compose (например, "-1")
|
// Отрезаем индекс реплики Docker Compose (например, "-1")
|
||||||
if (servicePart.endsWith('-1')) {
|
if (servicePart.endsWith('-1')) {
|
||||||
servicePart = servicePart.substring(0, servicePart.length - 2);
|
servicePart = servicePart.substring(0, servicePart.length - 2);
|
||||||
}
|
}
|
||||||
@@ -291,7 +294,7 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Возвращает текущий сформированный домен для отображения
|
* Возвращает текущий сформированный домен для вывода в UI
|
||||||
*/
|
*/
|
||||||
getContainerDomain(containerName: string): string | null {
|
getContainerDomain(containerName: string): string | null {
|
||||||
const serviceName = this.getServiceName(containerName);
|
const serviceName = this.getServiceName(containerName);
|
||||||
@@ -302,7 +305,7 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Возвращает текущий порт сервиса
|
* Возвращает порт сервиса из конфигурации MongoDB
|
||||||
*/
|
*/
|
||||||
getContainerPort(containerName: string): number {
|
getContainerPort(containerName: string): number {
|
||||||
const serviceName = this.getServiceName(containerName);
|
const serviceName = this.getServiceName(containerName);
|
||||||
@@ -313,28 +316,31 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вход в режим редактирования плашки
|
* Активация режима редактирования. Теперь полностью безопасна.
|
||||||
*/
|
*/
|
||||||
startEditDomain(container: any) {
|
startEditDomain(container: any) {
|
||||||
const serviceName = this.getServiceName(container.name);
|
const serviceName = this.getServiceName(container.name);
|
||||||
if (!serviceName) return;
|
if (!serviceName) {
|
||||||
|
console.warn(`[Jambotron] Не удалось определить имя сервиса для: ${container.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переключаем реактивный флаг видимости формы в шаблоне
|
||||||
this.editingDomains[container.id] = true;
|
this.editingDomains[container.id] = true;
|
||||||
|
|
||||||
// Инициализируем временные переменные текущими значениями из Mongo конфигурации
|
// Подтягиваем текущие сохранённые значения из локального объекта конфигурации config
|
||||||
const currentConfig = this.config?.serviceConfigs?.[serviceName];
|
const currentConfig = this.config?.serviceConfigs?.[serviceName];
|
||||||
this.tempSubdomain = currentConfig?.subdomain || '';
|
this.tempSubdomain = currentConfig?.subdomain || '';
|
||||||
this.tempPort = currentConfig?.port || 80;
|
this.tempPort = currentConfig?.port || 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранение изменённого домена и порта на бэкенд
|
* Сохранение изменённого поддомена и порта в MongoDB базы данных
|
||||||
*/
|
*/
|
||||||
saveDomainConfig(container: any) {
|
saveDomainConfig(container: any) {
|
||||||
const serviceName = this.getServiceName(container.name);
|
const serviceName = this.getServiceName(container.name);
|
||||||
if (!serviceName || !this.project?.id || !this.config) return;
|
if (!serviceName || !this.project?.id || !this.config) return;
|
||||||
|
|
||||||
// Гарантируем существование мапы конфигураций
|
|
||||||
if (!this.config.serviceConfigs) {
|
if (!this.config.serviceConfigs) {
|
||||||
this.config.serviceConfigs = {};
|
this.config.serviceConfigs = {};
|
||||||
}
|
}
|
||||||
@@ -342,7 +348,7 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
const subdomainClean = this.tempSubdomain.trim().toLowerCase();
|
const subdomainClean = this.tempSubdomain.trim().toLowerCase();
|
||||||
|
|
||||||
if (!subdomainClean) {
|
if (!subdomainClean) {
|
||||||
// Если поддомен стёрли — удаляем настройку проксирования для этого сервиса
|
// Если поле очищено — удаляем проксирование
|
||||||
delete this.config.serviceConfigs[serviceName];
|
delete this.config.serviceConfigs[serviceName];
|
||||||
} else {
|
} else {
|
||||||
this.config.serviceConfigs[serviceName] = {
|
this.config.serviceConfigs[serviceName] = {
|
||||||
@@ -351,15 +357,16 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем монолитный конфиг в базу MongoDB через твой сохранённый сервис
|
// Отправка монолитного конфига на бэкенд через твой рабочий ProjectService
|
||||||
this.projectService.updateProjectConfig(this.project.id, this.config).subscribe({
|
this.projectService.updateProjectConfig(this.project.id, this.config).subscribe({
|
||||||
next: (updatedConfig) => {
|
next: (updatedConfig) => {
|
||||||
this.config = updatedConfig;
|
this.config = updatedConfig;
|
||||||
this.editingDomains[container.id] = false;
|
this.editingDomains[container.id] = false;
|
||||||
console.log(`Конфигурация Caddy для ${serviceName} успешно зафиксирована.`);
|
console.log(`[Caddy] Маршрут для сервиса ${serviceName} успешно сохранён.`);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
alert(err.error?.message || 'Ошибка валидации доменного имени.');
|
console.error('Ошибка обновления конфигурации проекта:', err);
|
||||||
|
alert(err.error?.message || 'Не удалось применить конфигурацию шлюза.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,12 +155,13 @@ public class ProjectService {
|
|||||||
try {
|
try {
|
||||||
Files.createDirectories(projectPath);
|
Files.createDirectories(projectPath);
|
||||||
|
|
||||||
// Записываем уже модифицированный файл со всеми доменами
|
// Записываем модифицированный файл со всеми доменами
|
||||||
Files.writeString(projectPath.resolve("docker-compose.yml"), finalComposeContent);
|
Files.writeString(projectPath.resolve("docker-compose.yml"), finalComposeContent);
|
||||||
|
|
||||||
logToConsole(projectId, "Конфигурация Caddy подготовлена. Запуск контейнеров...");
|
logToConsole(projectId, "Конфигурация Caddy подготовлена. Запуск контейнеров...");
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder("docker", "compose", "up", "-d", "--remove-orphans");
|
// Добавляем --force-recreate, чтобы Docker гарантированно применил новые labels!
|
||||||
|
ProcessBuilder pb = new ProcessBuilder("docker", "compose", "up", "-d", "--force-recreate", "--remove-orphans");
|
||||||
pb.directory(projectPath.toFile());
|
pb.directory(projectPath.toFile());
|
||||||
Process process = pb.start();
|
Process process = pb.start();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user