diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34c511e..e2a0778 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## [1.5.0] - 2026-04-21
+### Added
+- Añadida gestión de múltiples ogBoot Servers
+- Si es un ogBoot existente, permite sincronizar su información sobre oglives y plantillas pxe.
+- En los Ajustes de red de equipos, aulas y grupos, ahora se debe seleccionar servidor ogBoot y luego oglive y plantilla dentro de dicho servidor.
+
## [1.4.0] - 2026-04-16
### Added
- Añadida gestión de múltiples Dhcps
diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts
index 75ce24e..0ef750c 100644
--- a/ogWebconsole/src/app/app-routing.module.ts
+++ b/ogWebconsole/src/app/app-routing.module.ts
@@ -36,6 +36,7 @@ import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component
import { MenusComponent } from "./components/menus/menus.component";
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
+import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
import {
RunScriptAssistantComponent
@@ -45,6 +46,7 @@ import { LogoutGuard } from './guards/logout.guard';
import { HardwareComponent } from './components/hardware/hardware.component';
import { HardwareProfileComponent } from './components/hardware-profile/hardware-profile.component';
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
+import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{
@@ -60,6 +62,8 @@ const routes: Routes = [
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'dhcp-servers', component: DhcpServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
+ { path: 'boot-servers', component: BootServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
+ { path: 'boot-servers/:uuid', component: BootServerDashboardComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts
index 922ca60..94b904b 100644
--- a/ogWebconsole/src/app/app.module.ts
+++ b/ogWebconsole/src/app/app.module.ts
@@ -58,10 +58,10 @@ import { MatProgressSpinner } from "@angular/material/progress-spinner";
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
-import { MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule } from "@angular/material/chips";
+import { MatChipsModule } from "@angular/material/chips";
import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component';
-import { MatTab, MatTabGroup } from "@angular/material/tabs";
-import { MatTooltip } from "@angular/material/tooltip";
+import { MatTabsModule } from "@angular/material/tabs";
+import { MatTooltipModule } from "@angular/material/tooltip";
import { MatExpansionModule } from '@angular/material/expansion';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ToastrModule } from 'ngx-toastr';
@@ -193,6 +193,13 @@ import { HardwareCollectionModalComponent } from './components/hardware-profile/
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component';
import { DiskPartitionSelectorComponent } from './components/groups/shared/disk-partition-selector/disk-partition-selector.component';
+import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
+import { ManageBootServerComponent } from './components/ogboot/manage-boot-server/manage-boot-server.component';
+import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
+import { OgcoreStatusComponent } from './components/global-status/ogcore-status/ogcore-status.component';
+import { GlobalOgbootStatusComponent } from './components/global-status/ogboot-status/ogboot-status.component';
+import { OgrepositoryStatusComponent } from './components/global-status/ogrepository-status/ogrepository-status.component';
+import { OgdhcpStatusComponent } from './components/global-status/ogdhcp-status/ogdhcp-status.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@@ -324,7 +331,14 @@ registerLocaleData(localeEs, 'es-ES');
HardwareCollectionModalComponent,
HardwareTypeComponent,
ShowDetailsComponent,
- AssetManagerModalComponent
+ AssetManagerModalComponent,
+ BootServersComponent,
+ ManageBootServerComponent,
+ BootServerDashboardComponent,
+ OgcoreStatusComponent,
+ GlobalOgbootStatusComponent,
+ OgrepositoryStatusComponent,
+ OgdhcpStatusComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,
@@ -349,7 +363,7 @@ registerLocaleData(localeEs, 'es-ES');
MatProgressBarModule,
MatStepperModule,
DragDropModule,
- MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
+ MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipsModule, MatProgressSpinner, MatTabsModule, MatTooltipModule,
MatExpansionModule,
NgxChartsModule,
MatDatepickerModule,
diff --git a/ogWebconsole/src/app/components/global-status/global-status.component.html b/ogWebconsole/src/app/components/global-status/global-status.component.html
index 890bebe..1d84466 100644
--- a/ogWebconsole/src/app/components/global-status/global-status.component.html
+++ b/ogWebconsole/src/app/components/global-status/global-status.component.html
@@ -26,7 +26,7 @@
}
- @if(!loading){
+ @if(!loading) {
storage
-
OgBoot Server
- @if(!errorOgBoot){
-
Estado: Operativo
- }
- @else{
-
Estado: Error
- }
+
Servidores OgBoot
+
Total: {{ bootServers.length }}
@@ -80,245 +75,24 @@
-
-
-
-
cloud_off
-
No hay repositorios disponibles
-
No se encontraron repositorios configurados en el sistema.
-
-
-
0" class="repositories-selector-container">
-
-
- Seleccionar repositorio
-
-
- {{ repository.name }}
-
-
- storage
-
-
-
-
-
-
-
storage
-
Selecciona un repositorio
-
Elige un repositorio de la lista para ver su estado detallado.
-
-
-
-
-
error_outline
-
Error de conexión
-
No se pudo conectar con el repositorio seleccionado
-
-
-
-
-
-
-
-
-
-
-
-
-
Cargando estado de OgBoot...
-
-
-
-
-
error_outline
-
Error de conexión
-
No se pudo conectar con el servidor OgBoot
-
-
-
-
-
- @if(selectedSection === 'dhcp'){
-
-
- @if(dhcps.length === 0){
-
-
cloud_off
-
No hay dhcps disponibles
-
No se encontraron dhcps configurados en el sistema.
-
- }
- @else {
-
-
-
- Seleccionar dhcp
-
-
- {{ dhcp.name }}
-
-
- storage
-
-
-
- @if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]){
-
- }
-
-
storage
-
Selecciona un dhcp
-
Elige un dhcp de la lista para ver su estado detallado.
-
-
-
-
-
error_outline
-
Error de conexión
-
No se pudo conectar con el dhcp seleccionado
-
-
-
-
- }
-
-
-
-
+ @if(selectedSection === 'repositories') {
+
}
+ @if(selectedSection === 'ogboot') {
+
+ }
+ @if(selectedSection === 'dhcp') {
+
+ }
-
-
-
-
+ @if(selectedSection === 'ogcore') {
+
+ }
}
-
diff --git a/ogWebconsole/src/app/components/global-status/global-status.component.ts b/ogWebconsole/src/app/components/global-status/global-status.component.ts
index 20d3896..ca4f2c1 100644
--- a/ogWebconsole/src/app/components/global-status/global-status.component.ts
+++ b/ogWebconsole/src/app/components/global-status/global-status.component.ts
@@ -2,7 +2,6 @@ import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
-import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-global-status',
@@ -12,13 +11,14 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
export class GlobalStatusComponent implements OnInit {
baseUrl: string;
loading: boolean = false;
- loadingOgBoot: boolean = false;
loadingDhcps: boolean = false;
loadingRepositories: boolean = false;
- errorOgBoot: boolean = false;
+ loadingBootServers: boolean = false;
+
errorDhcps: { [key: string]: boolean } = {};
errorRepositories: { [key: string]: boolean } = {};
- installedOgLives: any[] = [];
+ errorBootServers: { [key: string]: boolean } = {};
+
subnets: any[] = [];
showLabels: boolean = true;
isDoughnut: boolean = true;
@@ -26,62 +26,49 @@ export class GlobalStatusComponent implements OnInit {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
+
repositoriesUrl: string;
repositories: any[] = [];
repositoryStatuses: { [key: string]: any } = {};
- lastUpdateTime: string = '';
selectedRepositoryUuid: string = '';
- selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories';
-
- ogBootApiUrl: string;
- ogBootDiskUsage: any = {};
- ogBootServicesStatus: any = {};
- ogBootDiskUsageChartData: any[] = [];
dhcpsApiUrl: string;
dhcps: any[] = [];
- dhcpDiskUsage: any = {};
dhcpStatuses: { [key: string]: any } = {};
- dhcpDiskUsageChartData: any[] = [];
selectedDhcpUuid: string = '';
- isDhcp: boolean = false;
- isRepository: boolean = false;
+
+ bootServersApiUrl: string;
+ bootServers: any[] = [];
+ bootServerStatuses: { [key: string]: any } = {};
+ selectedBootServerUuid: string = '';
+
+ lastUpdateTime: string = '';
+ selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories';
ogCoreApiUrl: string;
- loadingOgBootOgLives: boolean = false;
- loadingOgBootServices: boolean = false;
- loadingOgBootDisk: boolean = false;
- loadingDhcpSubnets: boolean = false;
- loadingDhcpServices: boolean = false;
- loadingDhcpDisk: boolean = false;
-
constructor(
private configService: ConfigService,
private toastService: ToastrService,
- private http: HttpClient,
- private sanitizer: DomSanitizer
+ private http: HttpClient
) {
this.baseUrl = this.configService.apiUrl;
- this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`;
- this.ogCoreApiUrl = `${this.baseUrl}`;
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
-
- this.ogBootDiskUsageChartData = [];
- this.dhcpDiskUsageChartData = [];
+ this.bootServersApiUrl = `${this.baseUrl}/boot-servers`;
+ this.ogCoreApiUrl = `${this.baseUrl}`;
}
ngOnInit(): void {
+ this.loading = true;
this.updateLastUpdateTime();
- this.loadOgBootStatus();
- this.loadDhcps(false);
- this.loadRepositories(false);
+ this.loadDhcps(true);
+ this.loadRepositories(true);
+ this.loadBootServers(true);
+ this.loadBootServers(false);
this.syncSubnets();
- this.syncTemplates();
- this.syncOgLives();
}
syncSubnets() {
@@ -124,397 +111,145 @@ export class GlobalStatusComponent implements OnInit {
}
syncTemplates() {
- const timeoutId = setTimeout(() => {
- this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
- }, 3500);
+ if (this.bootServers.length === 0) return;
- this.http.post(`${this.baseUrl}/pxe-templates/sync`, {})
- .subscribe(response => {
- clearTimeout(timeoutId);
- this.toastService.success('Sincronización de las plantillas Pxe completada');
- }, error => {
- clearTimeout(timeoutId);
- this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las plantillas Pxe');
+ let pending = this.bootServers.length;
+ let hasError = false;
+
+ const timeoutId = setTimeout(() => {
+ if (!hasError) {
+ this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
+ hasError = true;
+ }
+ }, 5000);
+
+ this.bootServers.forEach(server => {
+ this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/pxe-templates/sync`, {}).subscribe({
+ next: () => {
+ pending--;
+ if (pending === 0 && !hasError) {
+ clearTimeout(timeoutId);
+ this.toastService.success('Sincronización de plantillas Pxe exitosa');
+ }
+ },
+ error: (error) => {
+ pending--;
+ if (!hasError) {
+ hasError = true;
+ clearTimeout(timeoutId);
+ this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar plantillas Pxe del servidor ${server.name || server.uuid}`);
+ }
+ }
});
+ });
}
syncOgLives(): void {
- const timeoutId = setTimeout(() => {
- this.toastService.error('Error al sincronizar las imagenes ogLive : tiempo de espera agotado');
- }, 3500);
+ if (this.bootServers.length === 0) return;
- this.http.post(`${this.baseUrl}/og-lives/sync`, {})
- .subscribe(response => {
- clearTimeout(timeoutId);
- this.toastService.success('Sincronización con los ogLives completada');
- }, error => {
- clearTimeout(timeoutId);
- this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las imagenes ogLive');
+ let pending = this.bootServers.length;
+ let hasError = false;
+
+ const timeoutId = setTimeout(() => {
+ if (!hasError) {
+ this.toastService.error('Error al sincronizar imagenes ogLive: tiempo de espera agotado');
+ hasError = true;
+ }
+ }, 5000);
+
+ this.bootServers.forEach(server => {
+ this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/oglives/sync`, {}).subscribe({
+ next: () => {
+ pending--;
+ if (pending === 0 && !hasError) {
+ clearTimeout(timeoutId);
+ this.toastService.success('Sincronización con ogLives completada');
+ }
+ },
+ error: (error) => {
+ pending--;
+ if (!hasError) {
+ hasError = true;
+ clearTimeout(timeoutId);
+ this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar ogLives del servidor ${server.name || server.uuid}`);
+ }
+ }
});
- }
-
- [key: string]: any;
-
- loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string, showLoading: boolean = true): void {
- if (isDhcp) {
- this.loadingDhcps = true;
- } else {
- this.loadingOgBoot = true;
- }
-
- if (showLoading) {
- this.loading = true;
- }
- this[errorState] = false;
- const timeoutId = setTimeout(() => {
- if (showLoading) {
- this.loading = false;
- }
- if (isDhcp) {
- this.loadingDhcps = false;
- } else {
- this.loadingOgBoot = false;
- }
- this[errorState] = true;
- }, 3500);
- this.http.get(apiUrl).subscribe({
- next: data => {
- diskUsage.used = data.message.disk_usage.used;
- diskUsage.available = data.message.disk_usage.available;
- diskUsage.total = data.message.disk_usage.total;
- diskUsage.percentage = data.message.disk_usage.percentage;
-
- Object.assign(servicesStatus, data.message.services_status);
-
- if (isDhcp) {
- this.subnets.length = 0;
- if (data.message.subnets) {
- this.subnets.push(...data.message.subnets);
- }
- } else {
- installedOgLives.length = 0;
- if (data.message.installed_oglives) {
- installedOgLives.push(...data.message.installed_oglives);
- }
- this.loadingOgBootOgLives = false;
- }
-
- diskUsageChartData.length = 0;
- diskUsageChartData.push(
- { name: 'Usado', value: parseFloat(diskUsage.used) },
- { name: 'Disponible', value: parseFloat(diskUsage.available) }
- );
-
- if (showLoading) {
- this.loading = false;
- }
- if (isDhcp) {
- this.loadingDhcps = false;
- } else {
- this.loadingOgBoot = false;
- }
- clearTimeout(timeoutId);
- },
- error: error => {
- this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado de ogBoot');
- if (showLoading) {
- this.loading = false;
- }
- if (isDhcp) {
- this.loadingDhcps = false;
- } else {
- this.loadingOgBoot = false;
- }
- this[errorState] = true;
- clearTimeout(timeoutId);
- }
});
}
loadRepositories(showLoading: boolean = true): void {
- if (showLoading) {
- this.loading = true;
- }
- this.errorRepositories = {};
-
- this.http.get(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe(
- data => {
+ if (showLoading) this.loadingRepositories = true;
+ this.http.get(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe({
+ next: data => {
this.repositories = data['hydra:member'] || [];
-
- if (this.repositories.length === 0) {
- if (showLoading) {
- this.loading = false;
- }
- return;
- }
-
- let remainingRepositories = this.repositories.length;
- const timeoutId = setTimeout(() => {
- if (showLoading) {
- this.loading = false;
- }
- this.repositories.forEach(repository => {
- if (!(repository.uuid in this.errorRepositories)) {
- this.errorRepositories[repository.uuid] = true;
- }
- });
- }, 5000);
-
- this.repositories.forEach(repository => {
- this.loadRepositoryStatus(repository.uuid, (errorOccurred: boolean) => {
- remainingRepositories--;
-
- this.errorRepositories[repository.uuid] = errorOccurred;
-
- if (remainingRepositories === 0) {
- if (showLoading) {
- this.loading = false;
- }
- clearTimeout(timeoutId);
- }
- });
- });
+ if (showLoading) this.loadingRepositories = false;
+ this.checkOverallLoading();
},
- error => {
- this.loading = false;
+ error: () => {
this.repositories = [];
+ if (showLoading) this.loadingRepositories = false;
+ this.checkOverallLoading();
this.toastService.error('Error al cargar la lista de repositorios');
}
- );
+ });
}
- loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
- console.log(`Cargando estado del repositorio: ${repositoryUuid}`);
- const timeoutId = setTimeout(() => {
- callback(true);
- }, 5000);
-
- this.http.get(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
- data => {
-
- if (!data.success) {
- clearTimeout(timeoutId);
- callback(true);
- return;
+ loadBootServers(showLoading: boolean = true): void {
+ if (showLoading) this.loadingBootServers = true;
+ this.http.get(`${this.bootServersApiUrl}?page=1&itemsPerPage=10`).subscribe({
+ next: data => {
+ this.bootServers = data['hydra:member'] || [];
+ if (showLoading) this.loadingBootServers = false;
+
+ if (this.bootServers.length > 0) {
+ this.syncTemplates();
+ this.syncOgLives();
}
-
- const details = data.details;
- const { disk, services, ram, cpu, processes } = details;
-
- this.repositoryStatuses[repositoryUuid] = {
- disk: {
- ...disk,
- used: parseFloat(disk.used.replace('GB', '')),
- available: parseFloat(disk.available.replace('GB', '')),
- total: parseFloat(disk.total.replace('GB', '')),
- percentage: disk.used_percentage
- },
- ram: {
- ...ram,
- used: parseFloat(ram.used.replace('GB', '')),
- available: parseFloat(ram.available.replace('GB', '')),
- total: parseFloat(ram.total.replace('GB', '')),
- percentage: ram.used_percentage
- },
- cpu: {
- used_percentage: cpu.used_percentage
- },
- services: services,
- processes: processes
- };
- clearTimeout(timeoutId);
- callback(false);
+
+ this.checkOverallLoading();
},
- error => {
- this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
- clearTimeout(timeoutId);
- callback(true);
+ error: () => {
+ this.bootServers = [];
+ if (showLoading) this.loadingBootServers = false;
+ this.checkOverallLoading();
+ this.toastService.error('Error al cargar la lista de Servidores de Boot');
}
- );
+ });
}
- loadOgBootStatus(): void {
- this.isDhcp = false;
- this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot', false);
- }
-
-
loadDhcps(showLoading: boolean = true): void {
- if (showLoading) {
- this.loading = true;
- }
- this.errorDhcps = {};
-
- this.http.get(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(
- data => {
+ if (showLoading) this.loadingDhcps = true;
+ this.http.get(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe({
+ next: data => {
this.dhcps = data['hydra:member'] || [];
-
- if (this.dhcps.length === 0) {
- if (showLoading) {
- this.loading = false;
- }
- return;
- }
-
- let remainingDhcps = this.dhcps.length;
- const timeoutId = setTimeout(() => {
- if (showLoading) {
- this.loading = false;
- }
- this.dhcps.forEach(dhcp => {
- if (!(dhcp.uuid in this.errorDhcps)) {
- this.errorDhcps[dhcp.uuid] = true;
- }
- });
- }, 5000);
-
- this.dhcps.forEach(dhcp => {
- this.loadDhcpStatus(dhcp.uuid, (errorOccurred: boolean) => {
- remainingDhcps--;
-
- this.errorDhcps[dhcp.uuid] = errorOccurred;
-
- if (remainingDhcps === 0) {
- if (showLoading) {
- this.loading = false;
- }
- clearTimeout(timeoutId);
- }
- });
- });
+ if (showLoading) this.loadingDhcps = false;
+ this.checkOverallLoading();
},
- error => {
- this.loading = false;
+ error: () => {
this.dhcps = [];
+ if (showLoading) this.loadingDhcps = false;
+ this.checkOverallLoading();
this.toastService.error('Error al cargar la lista de Dhcps');
}
- );
+ });
}
- loadDhcpStatus(dhcpUuid: string, callback: (errorOccurred: boolean) => void): void {
- console.log(`Cargando estado del Dhcp: ${dhcpUuid}`);
- const timeoutId = setTimeout(() => {
- callback(true);
- }, 5000);
-
- this.http.get(`${this.baseUrl}/dhcp-servers/server/${dhcpUuid}/status`).subscribe(
- data => {
-
- if (!data.success) {
- clearTimeout(timeoutId);
- callback(true);
- return;
- }
- const disk = data.message.disk_usage;
- const services = data.message.services_status;
- let diskUsageChartData: any[] = [];
- diskUsageChartData.length = 0;
- diskUsageChartData.push(
- { name: 'Usado', value: parseFloat(disk.used) },
- { name: 'Disponible', value: parseFloat(disk.available) }
- );
-
- this.subnets.length = 0;
- if (data.message.subnets) {
- this.subnets.push(...data.message.subnets);
- }
-
- this.dhcpStatuses[dhcpUuid] = {
- disk: {
- ...disk,
- used: parseFloat(disk.used.replace('GB', '')),
- available: parseFloat(disk.available.replace('GB', '')),
- total: parseFloat(disk.total.replace('GB', '')),
- percentage: disk.used_percentage
- },
- services: services,
- diskUsageChartData: diskUsageChartData
- };
- clearTimeout(timeoutId);
- callback(false);
- },
- error => {
- this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del dhcp');
- clearTimeout(timeoutId);
- callback(true);
- }
- );
- }
-
-
- onDhcpChange(dhcpUuid: string): void {
- this.selectedDhcpUuid = dhcpUuid;
- if (dhcpUuid && !this.dhcpStatuses[dhcpUuid]) {
- this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
- if (errorOccurred) {
- this.errorDhcps[dhcpUuid] = true;
- }
- });
+ checkOverallLoading(): void {
+ if (!this.loadingRepositories && !this.loadingBootServers && !this.loadingDhcps) {
+ this.loading = false;
}
}
- getSelectedDhcpName(): string {
- const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
- return selectedDhcp ? selectedDhcp.name : '';
- }
- onRepositoryChange(repositoryUuid: string): void {
- this.selectedRepositoryUuid = repositoryUuid;
- if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
- this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
- if (errorOccurred) {
- this.errorRepositories[repositoryUuid] = true;
- }
- });
- }
- }
-
- getSelectedRepositoryName(): string {
- const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
- return selectedRepo ? selectedRepo.name : '';
- }
-
- getRepositoryIframeUrl(): SafeResourceUrl {
- const repositoryName = this.getSelectedRepositoryName();
- if (repositoryName) {
- const encodedName = encodeURIComponent(repositoryName);
- const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
- return this.sanitizer.bypassSecurityTrustResourceUrl(url);
- }
- return this.sanitizer.bypassSecurityTrustResourceUrl('');
- }
-
- getOgBootIframeUrl(): SafeResourceUrl {
- const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
- return this.sanitizer.bypassSecurityTrustResourceUrl(url);
- }
-
- getDhcpIframeUrl(): SafeResourceUrl {
- const dhcpName = this.getSelectedDhcpName();
- if (dhcpName) {
- const encodedName = encodeURIComponent(dhcpName);
- const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
- return this.sanitizer.bypassSecurityTrustResourceUrl(url);
- }
- return this.sanitizer.bypassSecurityTrustResourceUrl('');
-
- }
-
- getOgCoreIframeUrl(): SafeResourceUrl {
- const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
- return this.sanitizer.bypassSecurityTrustResourceUrl(url);
- }
-
refreshAll(): void {
this.loading = true;
this.updateLastUpdateTime();
- this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, false, 'errorOgBoot', true);
+ this.loadBootServers(true);
this.loadDhcps(true);
this.loadRepositories(true);
this.syncSubnets();
- this.syncTemplates();
- this.syncOgLives();
this.toastService.success('Datos actualizados correctamente');
}
@@ -536,88 +271,11 @@ export class GlobalStatusComponent implements OnInit {
});
}
- retryRepositoryStatus(repositoryUuid: string): void {
- this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
- this.errorRepositories[repositoryUuid] = errorOccurred;
- if (!errorOccurred) {
- this.toastService.success('Estado del repositorio actualizado correctamente');
- } else {
- this.toastService.error('Error al cargar el estado del repositorio');
- }
- });
- }
-
- retryDhcpStatus(dhcpUuid: string): void {
- this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
- this.errorDhcps[dhcpUuid] = errorOccurred;
- if (!errorOccurred) {
- this.toastService.success('Estado del dhcp actualizado correctamente');
- } else {
- this.toastService.error('Error al cargar el estado del dhcp');
- }
- });
- }
-
selectSection(section: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore'): void {
this.selectedSection = section;
-
- switch (section) {
- case 'repositories':
- break;
- case 'ogboot':
- if (!this.ogBootDiskUsage) {
- this.loadOgBootStatus();
- }
- break;
- case 'dhcp':
- break;
- case 'ogcore':
- break;
-
- }
}
- scrollToLogs(): void {
- let logsSectionId: string;
- switch (this.selectedSection) {
- case 'repositories':
- logsSectionId = 'repository-logs-section';
- break;
- case 'ogboot':
- logsSectionId = 'ogboot-logs-section';
- break;
- case 'dhcp':
- logsSectionId = 'dhcp-logs-section';
- break;
- case 'ogcore':
- logsSectionId = 'ogcore-logs-section';
- break;
- default:
- return;
- }
- const checkAndScroll = () => {
- const logsSection = document.getElementById(logsSectionId);
- if (logsSection) {
- logsSection.scrollIntoView({
- behavior: 'smooth',
- block: 'start'
- });
- return true;
- }
- return false;
- };
-
- const interval = setInterval(() => {
- if (checkAndScroll()) {
- clearInterval(interval);
- }
- }, 100);
-
- setTimeout(() => {
- clearInterval(interval);
- }, 3000);
- }
}
diff --git a/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.css b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.css
new file mode 100644
index 0000000..46d0462
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.css
@@ -0,0 +1,886 @@
+/* ===== HEADER DE BIENVENIDA ===== */
+.welcome-header {
+ background: #3f51b5;
+ color: white;
+ padding: 2rem 2rem 1.5rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ position: relative;
+ overflow: hidden;
+}
+
+.welcome-header::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,');
+ opacity: 0.2;
+}
+
+.welcome-content {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.welcome-icon {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.welcome-icon mat-icon {
+ font-size: 32px;
+ width: 32px;
+ height: 32px;
+ color: white;
+}
+
+.welcome-text {
+ flex: 1;
+}
+
+.welcome-title {
+ margin: 0 0 0.5rem 0;
+ font-size: 2rem;
+ font-weight: 700;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.welcome-subtitle {
+ margin: 0 0 0.25rem 0;
+ font-size: 1.1rem;
+ font-weight: 500;
+ opacity: 0.95;
+}
+
+.welcome-description {
+ margin: 0;
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+.welcome-actions {
+ display: flex;
+ gap: 0.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.help-button,
+.refresh-button {
+ background: rgba(255, 255, 255, 0.2) !important;
+ color: white !important;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ transition: all 0.3s ease;
+}
+
+.help-button:hover,
+.refresh-button:hover {
+ background: rgba(255, 255, 255, 0.3) !important;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+/* ===== CONTENIDO PRINCIPAL ===== */
+mat-dialog-content {
+ height: calc(100% - 200px);
+ overflow: auto;
+ padding: 0 !important;
+ background: #f8f9fa;
+}
+
+.main-content {
+ padding: 2rem;
+}
+
+/* ===== SPINNER DE CARGA ===== */
+.spinner-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1000;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-content {
+ text-align: center;
+}
+
+.loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== RESUMEN DEL SISTEMA ===== */
+.system-overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.overview-card {
+ background: white;
+ border-radius: 16px;
+ padding: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+}
+
+.overview-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.overview-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+ border-color: #667eea;
+}
+
+.overview-card:hover::before {
+ opacity: 1;
+}
+
+.overview-card.clickable {
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.overview-card.clickable:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+}
+
+.overview-card.clickable:active {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
+}
+
+.overview-icon {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.overview-icon mat-icon {
+ color: white;
+ font-size: 24px;
+ width: 24px;
+ height: 24px;
+}
+
+.overview-content h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+.overview-content p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.click-hint {
+ margin-top: 0.5rem !important;
+ font-size: 0.75rem !important;
+ color: #667eea !important;
+ font-weight: 500;
+ opacity: 0.8;
+ transition: opacity 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.hint-icon {
+ font-size: 0.875rem !important;
+ width: 0.875rem !important;
+ height: 0.875rem !important;
+}
+
+.overview-card.clickable:hover .click-hint {
+ opacity: 1;
+}
+
+/* ===== BADGES DE ESTADO ===== */
+.status-badge {
+ display: inline-block;
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.status-badge.online {
+ background: linear-gradient(135deg, #28a745, #20c997);
+ color: white;
+}
+
+.status-badge.offline {
+ background: linear-gradient(135deg, #dc3545, #c82333);
+ color: white;
+}
+
+.status-badge.info {
+ background: linear-gradient(135deg, #17a2b8, #138496);
+ color: white;
+}
+
+/* ===== TABS PRINCIPALES ===== */
+.main-tabs {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+}
+
+::ng-deep .main-tabs .mat-tab-header {
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+}
+
+::ng-deep .main-tabs .mat-tab-label {
+ font-weight: 600;
+ color: #6c757d;
+ transition: all 0.3s ease;
+}
+
+::ng-deep .main-tabs .mat-tab-label-active {
+ color: #667eea;
+}
+
+::ng-deep .main-tabs .mat-ink-bar {
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ height: 3px;
+}
+
+.tab-content {
+ padding: 2rem;
+}
+
+/* ===== CONTENEDOR DE ERRORES ===== */
+.error-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.error-card {
+ background: white;
+ border-radius: 16px;
+ padding: 3rem 2rem;
+ text-align: center;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border: 1px solid #e9ecef;
+ max-width: 400px;
+}
+
+.error-icon {
+ font-size: 64px;
+ width: 64px;
+ height: 64px;
+ color: #dc3545;
+ margin-bottom: 1rem;
+}
+
+.error-card h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.error-card p {
+ margin: 0 0 2rem 0;
+ color: #6c757d;
+ line-height: 1.5;
+}
+
+/* ===== REPOSITORIOS ===== */
+.repositories-container {
+ padding: 1rem;
+}
+
+.repositories-selector-container {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.repository-selector {
+ display: flex;
+ justify-content: center;
+ padding: 1rem 0;
+}
+
+.repository-content {
+ padding: 1rem;
+}
+
+.repository-select-field {
+ min-width: 300px;
+ max-width: 500px;
+ width: 100%;
+}
+
+.selected-repository-content {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.no-repository-selected {
+ text-align: center;
+ padding: 3rem 2rem;
+ color: #6c757d;
+}
+
+.no-repository-selected .no-data-icon {
+ font-size: 4rem;
+ width: 4rem;
+ height: 4rem;
+ margin-bottom: 1rem;
+ opacity: 0.5;
+}
+
+.no-repository-selected h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.no-repository-selected p {
+ margin: 0;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+/* ===== FOOTER CON ACCIONES ===== */
+.action-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem 2rem;
+ background: white;
+ border-top: 1px solid #e9ecef;
+ border-radius: 0 0 12px 12px;
+}
+
+.action-info {
+ flex: 1;
+}
+
+.last-update {
+ margin: 0;
+ font-size: 0.85rem;
+ color: #6c757d;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 1rem;
+}
+
+.secondary-button {
+ color: #6c757d !important;
+ border: 1px solid #dee2e6 !important;
+ transition: all 0.3s ease;
+}
+
+.secondary-button:hover {
+ background: #f8f9fa !important;
+ border-color: #667eea !important;
+ color: #667eea !important;
+}
+
+/* ===== RESPONSIVE ===== */
+@media (max-width: 768px) {
+ .welcome-header {
+ padding: 1.5rem 1rem 1rem 1rem;
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .welcome-content {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .welcome-title {
+ font-size: 1.5rem;
+ }
+
+ .welcome-subtitle {
+ font-size: 1rem;
+ }
+
+ .main-content {
+ padding: 1rem;
+ }
+
+ .system-overview {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .overview-card {
+ padding: 1rem;
+ }
+
+ .action-container {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .action-buttons {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .welcome-header {
+ padding: 1rem;
+ }
+
+ .welcome-icon {
+ width: 50px;
+ height: 50px;
+ }
+
+ .welcome-icon mat-icon {
+ font-size: 28px;
+ width: 28px;
+ height: 28px;
+ }
+
+ .welcome-title {
+ font-size: 1.25rem;
+ }
+
+ .overview-card {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .tab-content {
+ padding: 1rem;
+ }
+
+ .repository-select-field {
+ min-width: 250px;
+ max-width: 100%;
+ }
+}
+
+/* ===== ANIMACIONES ===== */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.system-overview,
+.main-tabs {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card:nth-child(1) { animation-delay: 0.1s; }
+.overview-card:nth-child(2) { animation-delay: 0.2s; }
+.overview-card:nth-child(3) { animation-delay: 0.3s; }
+
+/* ===== ESTILOS GLOBALES DEL DIALOG ===== */
+::ng-deep .mat-dialog-container {
+ border-radius: 16px !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+}
+
+::ng-deep .mat-dialog-content {
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+/* ===== LOADING INDIVIDUAL POR TAB ===== */
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.loading-container .loading-content {
+ text-align: center;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-container .loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-container .loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== SECCIÓN DE LOGS ===== */
+.logs-section {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border: 1px solid #e9ecef;
+}
+
+.logs-header {
+ margin-bottom: 1rem;
+}
+
+.logs-header h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.logs-header h3::before {
+ content: '';
+ width: 4px;
+ height: 20px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-header p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.logs-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 1rem;
+ border: 1px solid #e9ecef;
+}
+
+.ogboot-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+.dhcp-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+/* Responsive para logs */
+@media (max-width: 768px) {
+ .logs-section {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ }
+
+ .logs-container {
+ padding: 0.5rem;
+ }
+
+ .ogboot-logs-iframe {
+ width: 100% !important;
+ height: 150px !important;
+ }
+}
+
+/* ===== TARJETAS ACTIVAS ===== */
+.overview-card.clickable.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+}
+
+.overview-card.clickable.active .overview-icon {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .status-badge {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .click-hint {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.overview-card.clickable.active h3 {
+ color: white;
+}
+
+.overview-card.clickable.active p {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+/* ===== CONTENIDO DINÁMICO ===== */
+.dynamic-content {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.section-content {
+ padding: 2rem;
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.html b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.html
new file mode 100644
index 0000000..a2605c0
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.html
@@ -0,0 +1,83 @@
+
+
+ @if(bootServers.length === 0) {
+
+
cloud_off
+
No hay servidores de boot disponibles
+
No se encontraron servidores OgBoot configurados en el sistema.
+
+ } @else {
+
+
+
+ Seleccionar servidor OgBoot
+
+ @for(server of bootServers; track server.uuid) {
+
+ {{ server.name }}
+
+ }
+
+ storage
+
+
+
+ @if(selectedBootServerUuid && !errorBootServers[selectedBootServerUuid] && bootServerStatuses[selectedBootServerUuid]) {
+
+ }
+
+ @if(!selectedBootServerUuid) {
+
+
storage
+
Selecciona un servidor de boot
+
Elige un servidor de boot de la lista para ver su estado detallado.
+
+ }
+
+ @if(selectedBootServerUuid && errorBootServers[selectedBootServerUuid]) {
+
+
+
error_outline
+
Error de conexión
+
No se pudo conectar con el servidor OgBoot seleccionado
+
+
+
+ }
+
+ }
+
+
diff --git a/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.spec.ts b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.spec.ts
new file mode 100644
index 0000000..145205d
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OgbootStatusComponent } from './ogboot-status.component';
+
+describe('OgbootStatusComponent', () => {
+ let component: OgbootStatusComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [OgbootStatusComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(OgbootStatusComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.ts b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.ts
new file mode 100644
index 0000000..df837c2
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.ts
@@ -0,0 +1,150 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { ConfigService } from '@services/config.service';
+import { ToastrService } from "ngx-toastr";
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+
+@Component({
+ selector: 'app-global-ogboot-status',
+ templateUrl: './ogboot-status.component.html',
+ styleUrl: './ogboot-status.component.css'
+})
+export class GlobalOgbootStatusComponent implements OnInit {
+ baseUrl: string;
+ loadingBootServers: boolean = false;
+
+ @Input() bootServers: any[] = [];
+
+ bootServerStatuses: { [key: string]: any } = {};
+ selectedBootServerUuid: string = '';
+ errorBootServers: { [key: string]: boolean } = {};
+
+ showLabels: boolean = true;
+ isDoughnut: boolean = true;
+ colorScheme: any = {
+ domain: ['#df200d', '#26a700']
+ };
+ view: [number, number] = [400, 220];
+
+ constructor(
+ private configService: ConfigService,
+ private toastService: ToastrService,
+ private http: HttpClient,
+ private sanitizer: DomSanitizer
+ ) {
+ this.baseUrl = this.configService.apiUrl;
+ }
+
+ ngOnInit(): void {
+ }
+
+
+
+ loadBootServerStatus(bootServerUuid: string, callback: (errorOccurred: boolean) => void): void {
+ const timeoutId = setTimeout(() => {
+ callback(true);
+ }, 5000);
+
+ this.http.get(`${this.baseUrl}/boot-servers/server/${bootServerUuid}/status`).subscribe(
+ data => {
+ if (data.code && data.code !== 200) {
+ clearTimeout(timeoutId);
+ callback(true);
+ return;
+ }
+
+ const message = data.message || data;
+ const disk = message.disk_usage;
+ const services = message.services_status;
+ const installedOgLives = message.installed_oglives || [];
+
+ let diskUsageChartData: any[] = [];
+ diskUsageChartData.push(
+ { name: 'Usado', value: parseFloat(disk.used) },
+ { name: 'Disponible', value: parseFloat(disk.available) }
+ );
+
+ this.bootServerStatuses[bootServerUuid] = {
+ disk: {
+ ...disk,
+ used: parseFloat(disk.used.replace('GB', '')),
+ available: parseFloat(disk.available.replace('GB', '')),
+ total: parseFloat(disk.total.replace('GB', '')),
+ percentage: disk.used_percentage || disk.percentage
+ },
+ services: services,
+ installedOgLives: installedOgLives,
+ diskUsageChartData: diskUsageChartData
+ };
+ clearTimeout(timeoutId);
+ callback(false);
+ },
+ error => {
+ this.toastService.error(error.error?.['hydra:description'] || 'Error al cargar el estado del servidor de boot');
+ clearTimeout(timeoutId);
+ callback(true);
+ }
+ );
+ }
+
+ onBootServerChange(bootServerUuid: string): void {
+ this.selectedBootServerUuid = bootServerUuid;
+ if (bootServerUuid && !this.bootServerStatuses[bootServerUuid]) {
+ this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
+ if (errorOccurred) {
+ this.errorBootServers[bootServerUuid] = true;
+ }
+ });
+ }
+ }
+
+ getSelectedBootServerName(): string {
+ const selectedBootServer = this.bootServers.find(server => server.uuid === this.selectedBootServerUuid);
+ return selectedBootServer ? selectedBootServer.name : '';
+ }
+
+ getBootServerIframeUrl(): SafeResourceUrl {
+ const bootServerName = this.getSelectedBootServerName();
+ if (bootServerName) {
+ const encodedName = encodeURIComponent(bootServerName);
+ const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
+ return this.sanitizer.bypassSecurityTrustResourceUrl(url);
+ }
+ return this.sanitizer.bypassSecurityTrustResourceUrl('');
+ }
+
+ retryBootServerStatus(bootServerUuid: string): void {
+ this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
+ this.errorBootServers[bootServerUuid] = errorOccurred;
+ if (!errorOccurred) {
+ this.toastService.success('Estado del servidor de boot actualizado correctamente');
+ } else {
+ this.toastService.error('Error al cargar el estado del servidor de boot');
+ }
+ });
+ }
+
+ scrollToLogs(): void {
+ const checkAndScroll = () => {
+ const logsSection = document.getElementById('ogboot-logs-section');
+ if (logsSection) {
+ logsSection.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ return true;
+ }
+ return false;
+ };
+
+ const interval = setInterval(() => {
+ if (checkAndScroll()) {
+ clearInterval(interval);
+ }
+ }, 100);
+
+ setTimeout(() => {
+ clearInterval(interval);
+ }, 3000);
+ }
+}
diff --git a/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.css b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.css
new file mode 100644
index 0000000..46d0462
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.css
@@ -0,0 +1,886 @@
+/* ===== HEADER DE BIENVENIDA ===== */
+.welcome-header {
+ background: #3f51b5;
+ color: white;
+ padding: 2rem 2rem 1.5rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ position: relative;
+ overflow: hidden;
+}
+
+.welcome-header::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,');
+ opacity: 0.2;
+}
+
+.welcome-content {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.welcome-icon {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.welcome-icon mat-icon {
+ font-size: 32px;
+ width: 32px;
+ height: 32px;
+ color: white;
+}
+
+.welcome-text {
+ flex: 1;
+}
+
+.welcome-title {
+ margin: 0 0 0.5rem 0;
+ font-size: 2rem;
+ font-weight: 700;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.welcome-subtitle {
+ margin: 0 0 0.25rem 0;
+ font-size: 1.1rem;
+ font-weight: 500;
+ opacity: 0.95;
+}
+
+.welcome-description {
+ margin: 0;
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+.welcome-actions {
+ display: flex;
+ gap: 0.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.help-button,
+.refresh-button {
+ background: rgba(255, 255, 255, 0.2) !important;
+ color: white !important;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ transition: all 0.3s ease;
+}
+
+.help-button:hover,
+.refresh-button:hover {
+ background: rgba(255, 255, 255, 0.3) !important;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+/* ===== CONTENIDO PRINCIPAL ===== */
+mat-dialog-content {
+ height: calc(100% - 200px);
+ overflow: auto;
+ padding: 0 !important;
+ background: #f8f9fa;
+}
+
+.main-content {
+ padding: 2rem;
+}
+
+/* ===== SPINNER DE CARGA ===== */
+.spinner-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1000;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-content {
+ text-align: center;
+}
+
+.loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== RESUMEN DEL SISTEMA ===== */
+.system-overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.overview-card {
+ background: white;
+ border-radius: 16px;
+ padding: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+}
+
+.overview-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.overview-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+ border-color: #667eea;
+}
+
+.overview-card:hover::before {
+ opacity: 1;
+}
+
+.overview-card.clickable {
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.overview-card.clickable:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+}
+
+.overview-card.clickable:active {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
+}
+
+.overview-icon {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.overview-icon mat-icon {
+ color: white;
+ font-size: 24px;
+ width: 24px;
+ height: 24px;
+}
+
+.overview-content h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+.overview-content p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.click-hint {
+ margin-top: 0.5rem !important;
+ font-size: 0.75rem !important;
+ color: #667eea !important;
+ font-weight: 500;
+ opacity: 0.8;
+ transition: opacity 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.hint-icon {
+ font-size: 0.875rem !important;
+ width: 0.875rem !important;
+ height: 0.875rem !important;
+}
+
+.overview-card.clickable:hover .click-hint {
+ opacity: 1;
+}
+
+/* ===== BADGES DE ESTADO ===== */
+.status-badge {
+ display: inline-block;
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.status-badge.online {
+ background: linear-gradient(135deg, #28a745, #20c997);
+ color: white;
+}
+
+.status-badge.offline {
+ background: linear-gradient(135deg, #dc3545, #c82333);
+ color: white;
+}
+
+.status-badge.info {
+ background: linear-gradient(135deg, #17a2b8, #138496);
+ color: white;
+}
+
+/* ===== TABS PRINCIPALES ===== */
+.main-tabs {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+}
+
+::ng-deep .main-tabs .mat-tab-header {
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+}
+
+::ng-deep .main-tabs .mat-tab-label {
+ font-weight: 600;
+ color: #6c757d;
+ transition: all 0.3s ease;
+}
+
+::ng-deep .main-tabs .mat-tab-label-active {
+ color: #667eea;
+}
+
+::ng-deep .main-tabs .mat-ink-bar {
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ height: 3px;
+}
+
+.tab-content {
+ padding: 2rem;
+}
+
+/* ===== CONTENEDOR DE ERRORES ===== */
+.error-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.error-card {
+ background: white;
+ border-radius: 16px;
+ padding: 3rem 2rem;
+ text-align: center;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border: 1px solid #e9ecef;
+ max-width: 400px;
+}
+
+.error-icon {
+ font-size: 64px;
+ width: 64px;
+ height: 64px;
+ color: #dc3545;
+ margin-bottom: 1rem;
+}
+
+.error-card h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.error-card p {
+ margin: 0 0 2rem 0;
+ color: #6c757d;
+ line-height: 1.5;
+}
+
+/* ===== REPOSITORIOS ===== */
+.repositories-container {
+ padding: 1rem;
+}
+
+.repositories-selector-container {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.repository-selector {
+ display: flex;
+ justify-content: center;
+ padding: 1rem 0;
+}
+
+.repository-content {
+ padding: 1rem;
+}
+
+.repository-select-field {
+ min-width: 300px;
+ max-width: 500px;
+ width: 100%;
+}
+
+.selected-repository-content {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.no-repository-selected {
+ text-align: center;
+ padding: 3rem 2rem;
+ color: #6c757d;
+}
+
+.no-repository-selected .no-data-icon {
+ font-size: 4rem;
+ width: 4rem;
+ height: 4rem;
+ margin-bottom: 1rem;
+ opacity: 0.5;
+}
+
+.no-repository-selected h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.no-repository-selected p {
+ margin: 0;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+/* ===== FOOTER CON ACCIONES ===== */
+.action-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem 2rem;
+ background: white;
+ border-top: 1px solid #e9ecef;
+ border-radius: 0 0 12px 12px;
+}
+
+.action-info {
+ flex: 1;
+}
+
+.last-update {
+ margin: 0;
+ font-size: 0.85rem;
+ color: #6c757d;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 1rem;
+}
+
+.secondary-button {
+ color: #6c757d !important;
+ border: 1px solid #dee2e6 !important;
+ transition: all 0.3s ease;
+}
+
+.secondary-button:hover {
+ background: #f8f9fa !important;
+ border-color: #667eea !important;
+ color: #667eea !important;
+}
+
+/* ===== RESPONSIVE ===== */
+@media (max-width: 768px) {
+ .welcome-header {
+ padding: 1.5rem 1rem 1rem 1rem;
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .welcome-content {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .welcome-title {
+ font-size: 1.5rem;
+ }
+
+ .welcome-subtitle {
+ font-size: 1rem;
+ }
+
+ .main-content {
+ padding: 1rem;
+ }
+
+ .system-overview {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .overview-card {
+ padding: 1rem;
+ }
+
+ .action-container {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .action-buttons {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .welcome-header {
+ padding: 1rem;
+ }
+
+ .welcome-icon {
+ width: 50px;
+ height: 50px;
+ }
+
+ .welcome-icon mat-icon {
+ font-size: 28px;
+ width: 28px;
+ height: 28px;
+ }
+
+ .welcome-title {
+ font-size: 1.25rem;
+ }
+
+ .overview-card {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .tab-content {
+ padding: 1rem;
+ }
+
+ .repository-select-field {
+ min-width: 250px;
+ max-width: 100%;
+ }
+}
+
+/* ===== ANIMACIONES ===== */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.system-overview,
+.main-tabs {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card:nth-child(1) { animation-delay: 0.1s; }
+.overview-card:nth-child(2) { animation-delay: 0.2s; }
+.overview-card:nth-child(3) { animation-delay: 0.3s; }
+
+/* ===== ESTILOS GLOBALES DEL DIALOG ===== */
+::ng-deep .mat-dialog-container {
+ border-radius: 16px !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+}
+
+::ng-deep .mat-dialog-content {
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+/* ===== LOADING INDIVIDUAL POR TAB ===== */
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.loading-container .loading-content {
+ text-align: center;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-container .loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-container .loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== SECCIÓN DE LOGS ===== */
+.logs-section {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border: 1px solid #e9ecef;
+}
+
+.logs-header {
+ margin-bottom: 1rem;
+}
+
+.logs-header h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.logs-header h3::before {
+ content: '';
+ width: 4px;
+ height: 20px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-header p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.logs-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 1rem;
+ border: 1px solid #e9ecef;
+}
+
+.ogboot-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+.dhcp-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+/* Responsive para logs */
+@media (max-width: 768px) {
+ .logs-section {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ }
+
+ .logs-container {
+ padding: 0.5rem;
+ }
+
+ .ogboot-logs-iframe {
+ width: 100% !important;
+ height: 150px !important;
+ }
+}
+
+/* ===== TARJETAS ACTIVAS ===== */
+.overview-card.clickable.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+}
+
+.overview-card.clickable.active .overview-icon {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .status-badge {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .click-hint {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.overview-card.clickable.active h3 {
+ color: white;
+}
+
+.overview-card.clickable.active p {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+/* ===== CONTENIDO DINÁMICO ===== */
+.dynamic-content {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.section-content {
+ padding: 2rem;
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.html b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.html
new file mode 100644
index 0000000..2681e7f
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.html
@@ -0,0 +1,18 @@
+
diff --git a/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.spec.ts b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.spec.ts
new file mode 100644
index 0000000..634560b
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OgcoreStatusComponent } from './ogcore-status.component';
+
+describe('OgcoreStatusComponent', () => {
+ let component: OgcoreStatusComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [OgcoreStatusComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(OgcoreStatusComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.ts b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.ts
new file mode 100644
index 0000000..9d39136
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogcore-status/ogcore-status.component.ts
@@ -0,0 +1,21 @@
+import { Component } from '@angular/core';
+import { ConfigService } from '@services/config.service';
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+
+@Component({
+ selector: 'app-ogcore-status',
+ templateUrl: './ogcore-status.component.html',
+ styleUrl: './ogcore-status.component.css'
+})
+export class OgcoreStatusComponent {
+
+ constructor(
+ private configService: ConfigService,
+ private sanitizer: DomSanitizer
+ ) {}
+
+ getOgCoreIframeUrl(): SafeResourceUrl {
+ const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
+ return this.sanitizer.bypassSecurityTrustResourceUrl(url);
+ }
+}
diff --git a/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.css b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.css
new file mode 100644
index 0000000..46d0462
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.css
@@ -0,0 +1,886 @@
+/* ===== HEADER DE BIENVENIDA ===== */
+.welcome-header {
+ background: #3f51b5;
+ color: white;
+ padding: 2rem 2rem 1.5rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ position: relative;
+ overflow: hidden;
+}
+
+.welcome-header::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,');
+ opacity: 0.2;
+}
+
+.welcome-content {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.welcome-icon {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.welcome-icon mat-icon {
+ font-size: 32px;
+ width: 32px;
+ height: 32px;
+ color: white;
+}
+
+.welcome-text {
+ flex: 1;
+}
+
+.welcome-title {
+ margin: 0 0 0.5rem 0;
+ font-size: 2rem;
+ font-weight: 700;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.welcome-subtitle {
+ margin: 0 0 0.25rem 0;
+ font-size: 1.1rem;
+ font-weight: 500;
+ opacity: 0.95;
+}
+
+.welcome-description {
+ margin: 0;
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+.welcome-actions {
+ display: flex;
+ gap: 0.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.help-button,
+.refresh-button {
+ background: rgba(255, 255, 255, 0.2) !important;
+ color: white !important;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ transition: all 0.3s ease;
+}
+
+.help-button:hover,
+.refresh-button:hover {
+ background: rgba(255, 255, 255, 0.3) !important;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+/* ===== CONTENIDO PRINCIPAL ===== */
+mat-dialog-content {
+ height: calc(100% - 200px);
+ overflow: auto;
+ padding: 0 !important;
+ background: #f8f9fa;
+}
+
+.main-content {
+ padding: 2rem;
+}
+
+/* ===== SPINNER DE CARGA ===== */
+.spinner-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1000;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-content {
+ text-align: center;
+}
+
+.loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== RESUMEN DEL SISTEMA ===== */
+.system-overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.overview-card {
+ background: white;
+ border-radius: 16px;
+ padding: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+}
+
+.overview-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.overview-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+ border-color: #667eea;
+}
+
+.overview-card:hover::before {
+ opacity: 1;
+}
+
+.overview-card.clickable {
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.overview-card.clickable:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+}
+
+.overview-card.clickable:active {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
+}
+
+.overview-icon {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.overview-icon mat-icon {
+ color: white;
+ font-size: 24px;
+ width: 24px;
+ height: 24px;
+}
+
+.overview-content h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+.overview-content p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.click-hint {
+ margin-top: 0.5rem !important;
+ font-size: 0.75rem !important;
+ color: #667eea !important;
+ font-weight: 500;
+ opacity: 0.8;
+ transition: opacity 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.hint-icon {
+ font-size: 0.875rem !important;
+ width: 0.875rem !important;
+ height: 0.875rem !important;
+}
+
+.overview-card.clickable:hover .click-hint {
+ opacity: 1;
+}
+
+/* ===== BADGES DE ESTADO ===== */
+.status-badge {
+ display: inline-block;
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.status-badge.online {
+ background: linear-gradient(135deg, #28a745, #20c997);
+ color: white;
+}
+
+.status-badge.offline {
+ background: linear-gradient(135deg, #dc3545, #c82333);
+ color: white;
+}
+
+.status-badge.info {
+ background: linear-gradient(135deg, #17a2b8, #138496);
+ color: white;
+}
+
+/* ===== TABS PRINCIPALES ===== */
+.main-tabs {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+}
+
+::ng-deep .main-tabs .mat-tab-header {
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+}
+
+::ng-deep .main-tabs .mat-tab-label {
+ font-weight: 600;
+ color: #6c757d;
+ transition: all 0.3s ease;
+}
+
+::ng-deep .main-tabs .mat-tab-label-active {
+ color: #667eea;
+}
+
+::ng-deep .main-tabs .mat-ink-bar {
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ height: 3px;
+}
+
+.tab-content {
+ padding: 2rem;
+}
+
+/* ===== CONTENEDOR DE ERRORES ===== */
+.error-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.error-card {
+ background: white;
+ border-radius: 16px;
+ padding: 3rem 2rem;
+ text-align: center;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border: 1px solid #e9ecef;
+ max-width: 400px;
+}
+
+.error-icon {
+ font-size: 64px;
+ width: 64px;
+ height: 64px;
+ color: #dc3545;
+ margin-bottom: 1rem;
+}
+
+.error-card h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.error-card p {
+ margin: 0 0 2rem 0;
+ color: #6c757d;
+ line-height: 1.5;
+}
+
+/* ===== REPOSITORIOS ===== */
+.repositories-container {
+ padding: 1rem;
+}
+
+.repositories-selector-container {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.repository-selector {
+ display: flex;
+ justify-content: center;
+ padding: 1rem 0;
+}
+
+.repository-content {
+ padding: 1rem;
+}
+
+.repository-select-field {
+ min-width: 300px;
+ max-width: 500px;
+ width: 100%;
+}
+
+.selected-repository-content {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.no-repository-selected {
+ text-align: center;
+ padding: 3rem 2rem;
+ color: #6c757d;
+}
+
+.no-repository-selected .no-data-icon {
+ font-size: 4rem;
+ width: 4rem;
+ height: 4rem;
+ margin-bottom: 1rem;
+ opacity: 0.5;
+}
+
+.no-repository-selected h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.no-repository-selected p {
+ margin: 0;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+/* ===== FOOTER CON ACCIONES ===== */
+.action-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem 2rem;
+ background: white;
+ border-top: 1px solid #e9ecef;
+ border-radius: 0 0 12px 12px;
+}
+
+.action-info {
+ flex: 1;
+}
+
+.last-update {
+ margin: 0;
+ font-size: 0.85rem;
+ color: #6c757d;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 1rem;
+}
+
+.secondary-button {
+ color: #6c757d !important;
+ border: 1px solid #dee2e6 !important;
+ transition: all 0.3s ease;
+}
+
+.secondary-button:hover {
+ background: #f8f9fa !important;
+ border-color: #667eea !important;
+ color: #667eea !important;
+}
+
+/* ===== RESPONSIVE ===== */
+@media (max-width: 768px) {
+ .welcome-header {
+ padding: 1.5rem 1rem 1rem 1rem;
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .welcome-content {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .welcome-title {
+ font-size: 1.5rem;
+ }
+
+ .welcome-subtitle {
+ font-size: 1rem;
+ }
+
+ .main-content {
+ padding: 1rem;
+ }
+
+ .system-overview {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .overview-card {
+ padding: 1rem;
+ }
+
+ .action-container {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .action-buttons {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .welcome-header {
+ padding: 1rem;
+ }
+
+ .welcome-icon {
+ width: 50px;
+ height: 50px;
+ }
+
+ .welcome-icon mat-icon {
+ font-size: 28px;
+ width: 28px;
+ height: 28px;
+ }
+
+ .welcome-title {
+ font-size: 1.25rem;
+ }
+
+ .overview-card {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .tab-content {
+ padding: 1rem;
+ }
+
+ .repository-select-field {
+ min-width: 250px;
+ max-width: 100%;
+ }
+}
+
+/* ===== ANIMACIONES ===== */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.system-overview,
+.main-tabs {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card:nth-child(1) { animation-delay: 0.1s; }
+.overview-card:nth-child(2) { animation-delay: 0.2s; }
+.overview-card:nth-child(3) { animation-delay: 0.3s; }
+
+/* ===== ESTILOS GLOBALES DEL DIALOG ===== */
+::ng-deep .mat-dialog-container {
+ border-radius: 16px !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+}
+
+::ng-deep .mat-dialog-content {
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+/* ===== LOADING INDIVIDUAL POR TAB ===== */
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.loading-container .loading-content {
+ text-align: center;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-container .loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-container .loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== SECCIÓN DE LOGS ===== */
+.logs-section {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border: 1px solid #e9ecef;
+}
+
+.logs-header {
+ margin-bottom: 1rem;
+}
+
+.logs-header h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.logs-header h3::before {
+ content: '';
+ width: 4px;
+ height: 20px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-header p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.logs-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 1rem;
+ border: 1px solid #e9ecef;
+}
+
+.ogboot-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+.dhcp-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+/* Responsive para logs */
+@media (max-width: 768px) {
+ .logs-section {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ }
+
+ .logs-container {
+ padding: 0.5rem;
+ }
+
+ .ogboot-logs-iframe {
+ width: 100% !important;
+ height: 150px !important;
+ }
+}
+
+/* ===== TARJETAS ACTIVAS ===== */
+.overview-card.clickable.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+}
+
+.overview-card.clickable.active .overview-icon {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .status-badge {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .click-hint {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.overview-card.clickable.active h3 {
+ color: white;
+}
+
+.overview-card.clickable.active p {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+/* ===== CONTENIDO DINÁMICO ===== */
+.dynamic-content {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.section-content {
+ padding: 2rem;
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.html b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.html
new file mode 100644
index 0000000..8e09c1f
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.html
@@ -0,0 +1,83 @@
+
+
+ @if(dhcps.length === 0) {
+
+
cloud_off
+
No hay dhcps disponibles
+
No se encontraron dhcps configurados en el sistema.
+
+ } @else {
+
+
+
+ Seleccionar dhcp
+
+ @for(dhcp of dhcps; track dhcp.uuid) {
+
+ {{ dhcp.name }}
+
+ }
+
+ storage
+
+
+
+ @if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]) {
+
+ }
+
+ @if(!selectedDhcpUuid) {
+
+
storage
+
Selecciona un dhcp
+
Elige un dhcp de la lista para ver su estado detallado.
+
+ }
+
+ @if(selectedDhcpUuid && errorDhcps[selectedDhcpUuid]) {
+
+
+
error_outline
+
Error de conexión
+
No se pudo conectar con el dhcp seleccionado
+
+
+
+ }
+
+ }
+
+
diff --git a/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.spec.ts b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.spec.ts
new file mode 100644
index 0000000..d33b040
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OgdhcpStatusComponent } from './ogdhcp-status.component';
+
+describe('OgdhcpStatusComponent', () => {
+ let component: OgdhcpStatusComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [OgdhcpStatusComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(OgdhcpStatusComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.ts b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.ts
new file mode 100644
index 0000000..2581aa2
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogdhcp-status/ogdhcp-status.component.ts
@@ -0,0 +1,153 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { ConfigService } from '@services/config.service';
+import { ToastrService } from "ngx-toastr";
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+
+@Component({
+ selector: 'app-ogdhcp-status',
+ templateUrl: './ogdhcp-status.component.html',
+ styleUrl: './ogdhcp-status.component.css'
+})
+export class OgdhcpStatusComponent implements OnInit {
+ baseUrl: string;
+ loadingDhcps: boolean = false;
+
+ @Input() dhcps: any[] = [];
+
+ dhcpStatuses: { [key: string]: any } = {};
+ selectedDhcpUuid: string = '';
+ errorDhcps: { [key: string]: boolean } = {};
+ subnets: any[] = [];
+
+ showLabels: boolean = true;
+ isDoughnut: boolean = true;
+ colorScheme: any = {
+ domain: ['#df200d', '#26a700']
+ };
+ view: [number, number] = [400, 220];
+
+ constructor(
+ private configService: ConfigService,
+ private toastService: ToastrService,
+ private http: HttpClient,
+ private sanitizer: DomSanitizer
+ ) {
+ this.baseUrl = this.configService.apiUrl;
+ }
+
+ ngOnInit(): void {
+ }
+
+
+
+ loadDhcpStatus(dhcpUuid: string, callback: (errorOccurred: boolean) => void): void {
+ const timeoutId = setTimeout(() => {
+ callback(true);
+ }, 5000);
+
+ this.http.get(`${this.baseUrl}/dhcp-servers/server/${dhcpUuid}/status`).subscribe(
+ data => {
+
+ if (!data.success && !data.message) {
+ clearTimeout(timeoutId);
+ callback(true);
+ return;
+ }
+ const message = data.message || data;
+ const disk = message.disk_usage;
+ const services = message.services_status;
+ let diskUsageChartData: any[] = [];
+ diskUsageChartData.push(
+ { name: 'Usado', value: parseFloat(disk.used) },
+ { name: 'Disponible', value: parseFloat(disk.available) }
+ );
+
+ this.subnets.length = 0;
+ if (message.subnets) {
+ this.subnets.push(...message.subnets);
+ }
+
+ this.dhcpStatuses[dhcpUuid] = {
+ disk: {
+ ...disk,
+ used: parseFloat(disk.used.replace('GB', '')),
+ available: parseFloat(disk.available.replace('GB', '')),
+ total: parseFloat(disk.total.replace('GB', '')),
+ percentage: disk.used_percentage || disk.percentage
+ },
+ services: services,
+ diskUsageChartData: diskUsageChartData
+ };
+ clearTimeout(timeoutId);
+ callback(false);
+ },
+ error => {
+ this.toastService.error(error.error?.['hydra:description'] || 'Error al cargar el estado del dhcp');
+ clearTimeout(timeoutId);
+ callback(true);
+ }
+ );
+ }
+
+ onDhcpChange(dhcpUuid: string): void {
+ this.selectedDhcpUuid = dhcpUuid;
+ if (dhcpUuid && !this.dhcpStatuses[dhcpUuid]) {
+ this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
+ if (errorOccurred) {
+ this.errorDhcps[dhcpUuid] = true;
+ }
+ });
+ }
+ }
+
+ getSelectedDhcpName(): string {
+ const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
+ return selectedDhcp ? selectedDhcp.name : '';
+ }
+
+ getDhcpIframeUrl(): SafeResourceUrl {
+ const dhcpName = this.getSelectedDhcpName();
+ if (dhcpName) {
+ const encodedName = encodeURIComponent(dhcpName);
+ const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
+ return this.sanitizer.bypassSecurityTrustResourceUrl(url);
+ }
+ return this.sanitizer.bypassSecurityTrustResourceUrl('');
+ }
+
+ retryDhcpStatus(dhcpUuid: string): void {
+ this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
+ this.errorDhcps[dhcpUuid] = errorOccurred;
+ if (!errorOccurred) {
+ this.toastService.success('Estado del dhcp actualizado correctamente');
+ } else {
+ this.toastService.error('Error al cargar el estado del dhcp');
+ }
+ });
+ }
+
+ scrollToLogs(): void {
+ const checkAndScroll = () => {
+ const logsSection = document.getElementById('dhcp-logs-section');
+ if (logsSection) {
+ logsSection.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ return true;
+ }
+ return false;
+ };
+
+ const interval = setInterval(() => {
+ if (checkAndScroll()) {
+ clearInterval(interval);
+ }
+ }, 100);
+
+ setTimeout(() => {
+ clearInterval(interval);
+ }, 3000);
+ }
+}
diff --git a/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.css b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.css
new file mode 100644
index 0000000..46d0462
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.css
@@ -0,0 +1,886 @@
+/* ===== HEADER DE BIENVENIDA ===== */
+.welcome-header {
+ background: #3f51b5;
+ color: white;
+ padding: 2rem 2rem 1.5rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ position: relative;
+ overflow: hidden;
+}
+
+.welcome-header::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,');
+ opacity: 0.2;
+}
+
+.welcome-content {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.welcome-icon {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.welcome-icon mat-icon {
+ font-size: 32px;
+ width: 32px;
+ height: 32px;
+ color: white;
+}
+
+.welcome-text {
+ flex: 1;
+}
+
+.welcome-title {
+ margin: 0 0 0.5rem 0;
+ font-size: 2rem;
+ font-weight: 700;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.welcome-subtitle {
+ margin: 0 0 0.25rem 0;
+ font-size: 1.1rem;
+ font-weight: 500;
+ opacity: 0.95;
+}
+
+.welcome-description {
+ margin: 0;
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+.welcome-actions {
+ display: flex;
+ gap: 0.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.help-button,
+.refresh-button {
+ background: rgba(255, 255, 255, 0.2) !important;
+ color: white !important;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ transition: all 0.3s ease;
+}
+
+.help-button:hover,
+.refresh-button:hover {
+ background: rgba(255, 255, 255, 0.3) !important;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+/* ===== CONTENIDO PRINCIPAL ===== */
+mat-dialog-content {
+ height: calc(100% - 200px);
+ overflow: auto;
+ padding: 0 !important;
+ background: #f8f9fa;
+}
+
+.main-content {
+ padding: 2rem;
+}
+
+/* ===== SPINNER DE CARGA ===== */
+.spinner-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1000;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-content {
+ text-align: center;
+}
+
+.loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== RESUMEN DEL SISTEMA ===== */
+.system-overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.overview-card {
+ background: white;
+ border-radius: 16px;
+ padding: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+}
+
+.overview-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.overview-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+ border-color: #667eea;
+}
+
+.overview-card:hover::before {
+ opacity: 1;
+}
+
+.overview-card.clickable {
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.overview-card.clickable:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+}
+
+.overview-card.clickable:active {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
+}
+
+.overview-icon {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.overview-icon mat-icon {
+ color: white;
+ font-size: 24px;
+ width: 24px;
+ height: 24px;
+}
+
+.overview-content h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+.overview-content p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.click-hint {
+ margin-top: 0.5rem !important;
+ font-size: 0.75rem !important;
+ color: #667eea !important;
+ font-weight: 500;
+ opacity: 0.8;
+ transition: opacity 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.hint-icon {
+ font-size: 0.875rem !important;
+ width: 0.875rem !important;
+ height: 0.875rem !important;
+}
+
+.overview-card.clickable:hover .click-hint {
+ opacity: 1;
+}
+
+/* ===== BADGES DE ESTADO ===== */
+.status-badge {
+ display: inline-block;
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.status-badge.online {
+ background: linear-gradient(135deg, #28a745, #20c997);
+ color: white;
+}
+
+.status-badge.offline {
+ background: linear-gradient(135deg, #dc3545, #c82333);
+ color: white;
+}
+
+.status-badge.info {
+ background: linear-gradient(135deg, #17a2b8, #138496);
+ color: white;
+}
+
+/* ===== TABS PRINCIPALES ===== */
+.main-tabs {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+}
+
+::ng-deep .main-tabs .mat-tab-header {
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+}
+
+::ng-deep .main-tabs .mat-tab-label {
+ font-weight: 600;
+ color: #6c757d;
+ transition: all 0.3s ease;
+}
+
+::ng-deep .main-tabs .mat-tab-label-active {
+ color: #667eea;
+}
+
+::ng-deep .main-tabs .mat-ink-bar {
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ height: 3px;
+}
+
+.tab-content {
+ padding: 2rem;
+}
+
+/* ===== CONTENEDOR DE ERRORES ===== */
+.error-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.error-card {
+ background: white;
+ border-radius: 16px;
+ padding: 3rem 2rem;
+ text-align: center;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border: 1px solid #e9ecef;
+ max-width: 400px;
+}
+
+.error-icon {
+ font-size: 64px;
+ width: 64px;
+ height: 64px;
+ color: #dc3545;
+ margin-bottom: 1rem;
+}
+
+.error-card h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.error-card p {
+ margin: 0 0 2rem 0;
+ color: #6c757d;
+ line-height: 1.5;
+}
+
+/* ===== REPOSITORIOS ===== */
+.repositories-container {
+ padding: 1rem;
+}
+
+.repositories-selector-container {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.repository-selector {
+ display: flex;
+ justify-content: center;
+ padding: 1rem 0;
+}
+
+.repository-content {
+ padding: 1rem;
+}
+
+.repository-select-field {
+ min-width: 300px;
+ max-width: 500px;
+ width: 100%;
+}
+
+.selected-repository-content {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.no-repository-selected {
+ text-align: center;
+ padding: 3rem 2rem;
+ color: #6c757d;
+}
+
+.no-repository-selected .no-data-icon {
+ font-size: 4rem;
+ width: 4rem;
+ height: 4rem;
+ margin-bottom: 1rem;
+ opacity: 0.5;
+}
+
+.no-repository-selected h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-weight: 600;
+}
+
+.no-repository-selected p {
+ margin: 0;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+/* ===== FOOTER CON ACCIONES ===== */
+.action-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem 2rem;
+ background: white;
+ border-top: 1px solid #e9ecef;
+ border-radius: 0 0 12px 12px;
+}
+
+.action-info {
+ flex: 1;
+}
+
+.last-update {
+ margin: 0;
+ font-size: 0.85rem;
+ color: #6c757d;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 1rem;
+}
+
+.secondary-button {
+ color: #6c757d !important;
+ border: 1px solid #dee2e6 !important;
+ transition: all 0.3s ease;
+}
+
+.secondary-button:hover {
+ background: #f8f9fa !important;
+ border-color: #667eea !important;
+ color: #667eea !important;
+}
+
+/* ===== RESPONSIVE ===== */
+@media (max-width: 768px) {
+ .welcome-header {
+ padding: 1.5rem 1rem 1rem 1rem;
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .welcome-content {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .welcome-title {
+ font-size: 1.5rem;
+ }
+
+ .welcome-subtitle {
+ font-size: 1rem;
+ }
+
+ .main-content {
+ padding: 1rem;
+ }
+
+ .system-overview {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .overview-card {
+ padding: 1rem;
+ }
+
+ .action-container {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .action-buttons {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .welcome-header {
+ padding: 1rem;
+ }
+
+ .welcome-icon {
+ width: 50px;
+ height: 50px;
+ }
+
+ .welcome-icon mat-icon {
+ font-size: 28px;
+ width: 28px;
+ height: 28px;
+ }
+
+ .welcome-title {
+ font-size: 1.25rem;
+ }
+
+ .overview-card {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .tab-content {
+ padding: 1rem;
+ }
+
+ .repository-select-field {
+ min-width: 250px;
+ max-width: 100%;
+ }
+}
+
+/* ===== ANIMACIONES ===== */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.system-overview,
+.main-tabs {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card {
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.overview-card:nth-child(1) { animation-delay: 0.1s; }
+.overview-card:nth-child(2) { animation-delay: 0.2s; }
+.overview-card:nth-child(3) { animation-delay: 0.3s; }
+
+/* ===== ESTILOS GLOBALES DEL DIALOG ===== */
+::ng-deep .mat-dialog-container {
+ border-radius: 16px !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+}
+
+::ng-deep .mat-dialog-content {
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+/* ===== LOADING INDIVIDUAL POR TAB ===== */
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 2rem;
+}
+
+.loading-container .loading-content {
+ text-align: center;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 2rem;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.loading-container .loading-spinner {
+ width: 60px !important;
+ height: 60px !important;
+ margin-bottom: 1rem;
+}
+
+.loading-container .loading-text {
+ margin: 0;
+ color: #666;
+ font-weight: 500;
+}
+
+/* ===== SECCIÓN DE LOGS ===== */
+.logs-section {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border: 1px solid #e9ecef;
+}
+
+.logs-header {
+ margin-bottom: 1rem;
+}
+
+.logs-header h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.logs-header h3::before {
+ content: '';
+ width: 4px;
+ height: 20px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-header p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.logs-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 1rem;
+ border: 1px solid #e9ecef;
+}
+
+.ogboot-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+.dhcp-logs-iframe {
+ border: none;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background: white;
+}
+
+/* Responsive para logs */
+@media (max-width: 768px) {
+ .logs-section {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ }
+
+ .logs-container {
+ padding: 0.5rem;
+ }
+
+ .ogboot-logs-iframe {
+ width: 100% !important;
+ height: 150px !important;
+ }
+}
+
+/* ===== TARJETAS ACTIVAS ===== */
+.overview-card.clickable.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+}
+
+.overview-card.clickable.active .overview-icon {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .status-badge {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.overview-card.clickable.active .click-hint {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.overview-card.clickable.active h3 {
+ color: white;
+}
+
+.overview-card.clickable.active p {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+/* ===== CONTENIDO DINÁMICO ===== */
+.dynamic-content {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ animation: fadeInUp 0.6s ease-out;
+}
+
+.section-content {
+ padding: 2rem;
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
+
+/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e9ecef;
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-header h2::before {
+ content: '';
+ width: 4px;
+ height: 24px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+}
+
+.logs-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 25px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
+}
+
+.logs-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
+}
+
+.logs-button mat-icon {
+ font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+/* Responsive para el header de sección */
+@media (max-width: 768px) {
+ .section-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .section-header h2 {
+ font-size: 1.25rem;
+ }
+
+ .logs-button {
+ padding: 0.5rem 1.25rem;
+ font-size: 0.8rem;
+ }
+
+ .logs-button mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.html b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.html
new file mode 100644
index 0000000..bd85fd6
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.html
@@ -0,0 +1,91 @@
+
+
+ @if(repositories.length === 0) {
+
+
cloud_off
+
No hay repositorios disponibles
+
No se encontraron repositorios configurados en el sistema.
+
+ } @else {
+
+
+
+ Seleccionar repositorio
+
+ @for(repository of repositories; track repository.uuid) {
+
+ {{ repository.name }}
+
+ }
+
+ storage
+
+
+
+ @if(selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]) {
+
+ }
+
+ @if(!selectedRepositoryUuid) {
+
+
storage
+
Selecciona un repositorio
+
Elige un repositorio de la lista para ver su estado detallado.
+
+ }
+
+ @if(selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]) {
+
+
+
error_outline
+
Error de conexión
+
No se pudo conectar con el repositorio seleccionado
+
+
+
+ }
+
+ }
+
+
diff --git a/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.spec.ts b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.spec.ts
new file mode 100644
index 0000000..0611fc6
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OgrepositoryStatusComponent } from './ogrepository-status.component';
+
+describe('OgrepositoryStatusComponent', () => {
+ let component: OgrepositoryStatusComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [OgrepositoryStatusComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(OgrepositoryStatusComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.ts b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.ts
new file mode 100644
index 0000000..4e6e42c
--- /dev/null
+++ b/ogWebconsole/src/app/components/global-status/ogrepository-status/ogrepository-status.component.ts
@@ -0,0 +1,152 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { ConfigService } from '@services/config.service';
+import { ToastrService } from "ngx-toastr";
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+
+@Component({
+ selector: 'app-ogrepository-status',
+ templateUrl: './ogrepository-status.component.html',
+ styleUrl: './ogrepository-status.component.css'
+})
+export class OgrepositoryStatusComponent implements OnInit {
+ baseUrl: string;
+ loading: boolean = false;
+
+ @Input() repositories: any[] = [];
+
+ repositoryStatuses: { [key: string]: any } = {};
+ selectedRepositoryUuid: string = '';
+ errorRepositories: { [key: string]: boolean } = {};
+
+ showLabels: boolean = true;
+ isDoughnut: boolean = true;
+ colorScheme: any = {
+ domain: ['#df200d', '#26a700']
+ };
+ view: [number, number] = [400, 220];
+
+ constructor(
+ private configService: ConfigService,
+ private toastService: ToastrService,
+ private http: HttpClient,
+ private sanitizer: DomSanitizer
+ ) {
+ this.baseUrl = this.configService.apiUrl;
+ }
+
+ ngOnInit(): void {
+ // Parent passes repositories list
+ }
+
+ // removed loadRepositories function that fetched from API
+
+ loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
+ const timeoutId = setTimeout(() => {
+ callback(true);
+ }, 5000);
+
+ this.http.get(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
+ data => {
+ if (!data.success) {
+ clearTimeout(timeoutId);
+ callback(true);
+ return;
+ }
+
+ const details = data.details;
+ const { disk, services, ram, cpu, processes } = details;
+
+ this.repositoryStatuses[repositoryUuid] = {
+ disk: {
+ ...disk,
+ used: parseFloat(disk.used.replace('GB', '')),
+ available: parseFloat(disk.available.replace('GB', '')),
+ total: parseFloat(disk.total.replace('GB', '')),
+ percentage: disk.used_percentage
+ },
+ ram: {
+ ...ram,
+ used: parseFloat(ram.used.replace('GB', '')),
+ available: parseFloat(ram.available.replace('GB', '')),
+ total: parseFloat(ram.total.replace('GB', '')),
+ percentage: ram.used_percentage
+ },
+ cpu: {
+ used_percentage: cpu.used_percentage
+ },
+ services: services,
+ processes: processes
+ };
+ clearTimeout(timeoutId);
+ callback(false);
+ },
+ error => {
+ this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
+ clearTimeout(timeoutId);
+ callback(true);
+ }
+ );
+ }
+
+ onRepositoryChange(repositoryUuid: string): void {
+ this.selectedRepositoryUuid = repositoryUuid;
+ if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
+ this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
+ if (errorOccurred) {
+ this.errorRepositories[repositoryUuid] = true;
+ }
+ });
+ }
+ }
+
+ getSelectedRepositoryName(): string {
+ const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
+ return selectedRepo ? selectedRepo.name : '';
+ }
+
+ getRepositoryIframeUrl(): SafeResourceUrl {
+ const repositoryName = this.getSelectedRepositoryName();
+ if (repositoryName) {
+ const encodedName = encodeURIComponent(repositoryName);
+ const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
+ return this.sanitizer.bypassSecurityTrustResourceUrl(url);
+ }
+ return this.sanitizer.bypassSecurityTrustResourceUrl('');
+ }
+
+ retryRepositoryStatus(repositoryUuid: string): void {
+ this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
+ this.errorRepositories[repositoryUuid] = errorOccurred;
+ if (!errorOccurred) {
+ this.toastService.success('Estado del repositorio actualizado correctamente');
+ } else {
+ this.toastService.error('Error al cargar el estado del repositorio');
+ }
+ });
+ }
+
+ scrollToLogs(): void {
+ const checkAndScroll = () => {
+ const logsSection = document.getElementById('repository-logs-section');
+ if (logsSection) {
+ logsSection.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ return true;
+ }
+ return false;
+ };
+
+ const interval = setInterval(() => {
+ if (checkAndScroll()) {
+ clearInterval(interval);
+ }
+ }, 100);
+
+ setTimeout(() => {
+ clearInterval(interval);
+ }, 3000);
+ }
+}
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css
index 3ae50b9..30efd99 100644
--- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css
+++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css
@@ -53,4 +53,28 @@ mat-option .unit-path {
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
+}
+
+/* Boot grouping */
+.boot-group {
+ grid-column: 1 / -1;
+ border: 1px solid #c5c5c5;
+ border-radius: 6px;
+ padding: 12px 16px 4px 16px;
+ margin: 0;
+}
+
+.boot-group-legend {
+ font-size: 0.78rem;
+ color: #3f51b5;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ padding: 0 6px;
+ text-transform: uppercase;
+}
+
+.boot-group-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ column-gap: 20px;
}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html
index 75febb5..2c1cc0e 100644
--- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html
+++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html
@@ -3,16 +3,19 @@