caddy lables

This commit is contained in:
2026-05-22 09:50:33 +03:00
parent 584fa9e320
commit a4dc510c93
4 changed files with 74 additions and 62 deletions
@@ -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>
} }
@@ -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
}
}
@@ -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();