Resuelve conflicto de fusión en CHANGELOG.md
testing/ogGui-multibranch/pipeline/head Build started... Details
testing/ogGui-multibranch/pipeline/pr-devel Build queued... Details

pull/93/head
Juan Manuel Bardallo 2026-05-18 14:20:04 +02:00
commit cb1e6a0fa9
16 changed files with 473 additions and 44 deletions

View File

@ -1,5 +1,21 @@
# Changelog
## [1.7.0] - 2026-05-14
### Added
- Visor de logs de disco en las páginas de estado de ogcore, ogboot, ogdhcp
y ogrepository: muestra entries del journal en tiempo real sin necesidad
de oglog/OpenSearch, con polling cada 5 segundos y modo live/pausa
- Soporte de múltiples `SYSLOG_IDENTIFIER` por componente: ogdhcp incluye
kea, ogrepository incluye samba, ogboot incluye tftpd-hpa
- Host basado en IP del servidor seleccionado para lectura del fichero de
journal remoto (`/var/log/journal/remote/<IP>.journal`)
### Changed
- `StatusLogsComponent`: `@Input() identifier` reemplazado por
`@Input() identifiers: string[]`; respuesta parseada como array de entries
JSON estructurados con formato `[timestamp] IDENTIFIER: mensaje`
## [1.6.1] - 2026-05-14
### Improved
- Modificada recepcion de mensajes de mercure durante la importación

View File

@ -61,6 +61,19 @@ pipeline {
}
}
stage('Generate Changelog (Devel)') {
when {
branch 'devel'
}
steps {
script {
def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME
def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL
generateDebianChangelog(env.BUILD_DIR, devName, devEmail, "devel")
}
}
}
stage('Build') {
steps {
@ -97,20 +110,38 @@ pipeline {
}
}
}
stage ('Publish to Debian Repository (Devel)') {
when {
branch 'devel'
}
agent { label 'debian-repo' }
steps {
script {
def versionPattern = "-${env.BUILD_NUMBER}~devel"
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'devel', versionPattern)
}
}
}
}
post {
success {
script {
// Solo lanzar cuando el build sea exitoso y en la rama main
if (env.BRANCH_NAME == 'main') {
build job: 'Aptly publish nightly repository',
wait: false,
parameters: [
string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}")
]
build job: 'Aptly publish nightly repository',
wait: false,
parameters: [
string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}")
]
}
if (env.BRANCH_NAME == 'devel') {
build job: 'Aptly publish develop repository',
wait: false,
parameters: [
string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}")
]
}
}
}
always {
notifyBuildStatus('opengnsys@qindel.com')

View File

@ -201,6 +201,7 @@ import { OgcoreStatusComponent } from './components/global-status/ogcore-status/
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';
import { StatusLogsComponent } from './components/global-status/status-logs/status-logs.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@ -344,6 +345,7 @@ registerLocaleData(localeEs, 'es-ES');
GlobalOgbootStatusComponent,
OgrepositoryStatusComponent,
OgdhcpStatusComponent,
StatusLogsComponent,
ImportLegacyComponent,
PageNotFoundComponent
],

View File

@ -42,15 +42,13 @@
</div>
</div>
<div class="logs-section" id="ogboot-logs-section">
<div class="logs-header">
<h3>{{ 'ogbootLogs' | translate }}</h3>
<p>{{ 'realTimeOgbootLogs' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getBootServerIframeUrl()" width="100%" height="800" frameborder="0" class="ogboot-logs-iframe">
</iframe>
</div>
<div id="ogboot-logs-section">
<app-status-logs
[title]="'ogbootLogs'"
[identifiers]="['ogboot', 'tftpd-hpa']"
[host]="getSelectedBootServerIp()"
[oglogUrl]="getBootServerIframeUrl()">
</app-status-logs>
</div>
</div>
}

View File

@ -101,6 +101,11 @@ export class GlobalOgbootStatusComponent implements OnInit {
}
}
getSelectedBootServerIp(): string {
const s = this.bootServers.find(s => s.uuid === this.selectedBootServerUuid);
return s ? s.ip : '';
}
getSelectedBootServerName(): string {
const selectedBootServer = this.bootServers.find(server => server.uuid === this.selectedBootServerUuid);
return selectedBootServer ? selectedBootServer.name : '';

View File

@ -1,14 +1,12 @@
<div class="section-content">
<div class="tab-content">
<div class="logs-section" id="ogcore-logs-section">
<div class="logs-header">
<h3>{{ 'ogcoreLogs' | translate }}</h3>
<p>{{ 'realTimeOgcoreLogs' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getOgCoreIframeUrl()" width="100%" height="800" frameborder="0" class="ogcore-logs-iframe">
</iframe>
</div>
<div id="ogcore-logs-section">
<app-status-logs
[title]="'ogcoreLogs'"
[identifiers]="['ogcore']"
[host]="''"
[oglogUrl]="getOgCoreIframeUrl()">
</app-status-logs>
</div>
</div>
</div>

View File

@ -42,15 +42,13 @@
</div>
</div>
<div class="logs-section" id="dhcp-logs-section">
<div class="logs-header">
<h3>{{ 'dhcpLogs' | translate }}</h3>
<p>{{ 'realTimeDhcpLogs' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getDhcpIframeUrl()" width="100%" height="800" frameborder="0" class="dhcp-logs-iframe">
</iframe>
</div>
<div id="dhcp-logs-section">
<app-status-logs
[title]="'dhcpLogs'"
[identifiers]="['ogdhcp', 'kea-dhcp4', 'kea-ctrl-agent', 'DhcpLFC']"
[host]="getSelectedDhcpIp()"
[oglogUrl]="getDhcpIframeUrl()">
</app-status-logs>
</div>
</div>
}

View File

@ -104,6 +104,11 @@ export class OgdhcpStatusComponent implements OnInit {
}
}
getSelectedDhcpIp(): string {
const d = this.dhcps.find(d => d.uuid === this.selectedDhcpUuid);
return d ? d.ip : '';
}
getSelectedDhcpName(): string {
const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
return selectedDhcp ? selectedDhcp.name : '';

View File

@ -49,16 +49,13 @@
</div>
</div>
<div class="repository-logs-section" id="repository-logs-section">
<div class="logs-header">
<h3>Datos del repositorio: {{ getSelectedRepositoryName() }}</h3>
<p>{{ 'specificRepoInfo' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getRepositoryIframeUrl()" width="100%" height="600" frameborder="0"
class="repository-logs-iframe">
</iframe>
</div>
<div id="repository-logs-section">
<app-status-logs
[title]="'ogrepositoryLogs'"
[identifiers]="['ogrepo-api', 'ogrepo-api_DEBUG', '(smbd)', '(nmbd)']"
[host]="getSelectedRepositoryIp()"
[oglogUrl]="getRepositoryIframeUrl()">
</app-status-logs>
</div>
</div>
}

View File

@ -103,6 +103,11 @@ export class OgrepositoryStatusComponent implements OnInit {
}
}
getSelectedRepositoryIp(): string {
const r = this.repositories.find(r => r.uuid === this.selectedRepositoryUuid);
return r ? r.ip : '';
}
getSelectedRepositoryName(): string {
const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
return selectedRepo ? selectedRepo.name : '';

View File

@ -0,0 +1,165 @@
.logs-section {
background-color: var(--surface-card, #ffffff);
border-radius: 12px;
padding: 16px;
margin-top: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.logs-header {
margin-bottom: 12px;
}
.logs-header h3 {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
color: var(--text-primary, #111827);
}
.logs-header p {
font-size: 0.8rem;
color: var(--text-secondary, #6b7280);
margin: 2px 0 0;
}
.logs-tabs {
margin-top: 8px;
}
.logs-container {
position: relative;
background-color: #0d1117;
border-radius: 8px;
margin-top: 8px;
display: flex;
flex-direction: column;
}
.tab-actions-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 12px;
background: rgba(255, 255, 255, 0.05);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header-actions {
display: flex;
gap: 4px;
}
.header-actions button {
color: rgba(255, 255, 255, 0.7) !important;
}
.header-actions button:hover:not([disabled]) {
color: white !important;
background: rgba(255, 255, 255, 0.1);
}
.header-actions button[disabled] {
color: rgba(255, 255, 255, 0.2) !important;
}
.header-actions button[color="warn"] {
color: #ff4081 !important;
}
.live-badge {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
font-weight: 700;
color: #ff4081;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.live-dot {
width: 6px;
height: 6px;
background-color: #ff4081;
border-radius: 50%;
animation: pulse 2s infinite;
}
.log-viewer {
margin: 0;
padding: 12px;
color: #e6edf3;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.8rem;
line-height: 1.4;
overflow-y: auto;
max-height: 50vh;
white-space: pre-wrap;
word-break: break-all;
}
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(255, 64, 129, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 8px rgba(255, 64, 129, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(255, 64, 129, 0);
}
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(13, 17, 23, 0.7);
z-index: 10;
}
.error-message {
padding: 32px;
text-align: center;
color: #f85149;
}
.error-message mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
margin-bottom: 16px;
}
.logs-iframe {
border-radius: 8px;
}
.no-oglog {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px;
color: #8b949e;
}
.no-oglog mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
margin-bottom: 16px;
}

View File

@ -0,0 +1,59 @@
<div class="logs-section">
<div class="logs-header">
<h3>{{ title | translate }}</h3>
<p>{{ 'realTimeLogs' | translate }}</p>
</div>
<mat-tab-group class="logs-tabs" animationDuration="0ms" (selectedTabChange)="onTabChange($event)">
<mat-tab [label]="'diskLogs' | translate">
<div class="logs-container disk-logs">
<div class="tab-actions-header">
<div class="header-actions">
<button mat-icon-button
(click)="toggleLive()"
[matTooltip]="(isLive ? 'diskLogsStopLive' : 'diskLogsStartLive') | translate"
[color]="isLive ? 'warn' : ''">
<mat-icon>{{ isLive ? 'pause_circle' : 'play_circle' }}</mat-icon>
</button>
<button mat-icon-button (click)="refreshLogs()" [matTooltip]="'buttonUpdateData' | translate" [disabled]="isLive">
<mat-icon>refresh</mat-icon>
</button>
</div>
<div *ngIf="isLive" class="live-badge">
<span class="live-dot"></span>
LIVE
</div>
</div>
@if (loadingDiskLogs && !diskLogsContent) {
<div class="loading-overlay">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
@if (errorDiskLogs) {
<div class="error-message">
<mat-icon>error_outline</mat-icon>
<p>{{ errorDiskLogs }}</p>
<button mat-button (click)="loadDiskLogs()">{{ 'buttonRetry' | translate }}</button>
</div>
}
<pre #logContent class="log-viewer">{{ diskLogsContent }}</pre>
</div>
</mat-tab>
<mat-tab [label]="'oglogLogs' | translate">
<div class="logs-container oglog-logs">
@if (oglogUrl) {
<iframe [src]="oglogUrl" width="100%" height="600" frameborder="0" class="logs-iframe"></iframe>
} @else {
<div class="no-oglog">
<mat-icon>info_outline</mat-icon>
<p>oglog no está disponible o no se ha configurado la URL.</p>
</div>
}
</div>
</mat-tab>
</mat-tab-group>
</div>

View File

@ -0,0 +1,139 @@
import { Component, Input, OnInit, OnDestroy, inject, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SafeResourceUrl } from '@angular/platform-browser';
import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core';
import { MatTabChangeEvent } from '@angular/material/tabs';
interface JournalEntry {
MESSAGE: string;
SYSLOG_IDENTIFIER?: string;
_HOSTNAME?: string;
__REALTIME_TIMESTAMP?: string;
PRIORITY?: string;
}
@Component({
selector: 'app-status-logs',
templateUrl: './status-logs.component.html',
styleUrl: './status-logs.component.css'
})
export class StatusLogsComponent implements OnInit, OnDestroy, AfterViewChecked {
@ViewChild('logContent') private logContainer!: ElementRef;
@Input() identifiers: string[] = [];
@Input() host: string = '';
@Input() oglogUrl: SafeResourceUrl | null = null;
@Input() title: string = '';
public translate = inject(TranslateService);
diskLogsContent: string = '';
loadingDiskLogs: boolean = false;
errorDiskLogs: string | null = null;
isLive: boolean = true;
private pollInterval: any;
private readonly POLL_INTERVAL_MS = 5000;
constructor(
private http: HttpClient,
private configService: ConfigService
) {}
ngOnInit(): void {
this.loadDiskLogs();
if (this.isLive) {
this.startPolling();
}
}
ngAfterViewChecked() {
this.scrollToBottom();
}
toggleLive(): void {
this.isLive = !this.isLive;
if (this.isLive) {
this.loadDiskLogs();
this.startPolling();
} else {
this.stopPolling();
}
}
onTabChange(event: MatTabChangeEvent): void {
if (event.index === 0) {
if (this.isLive) {
this.loadDiskLogs();
this.startPolling();
}
} else {
this.stopPolling();
}
}
ngOnDestroy(): void {
this.stopPolling();
}
loadDiskLogs(): void {
if (!this.identifiers.length) return;
if (!this.isLive) {
this.loadingDiskLogs = true;
}
const idParam = this.identifiers.join(',');
const hostParam = this.host ? `&host=${encodeURIComponent(this.host)}` : '';
const url = `${this.configService.apiUrl}/opengnsys/system/disk-logs?SYSLOG_IDENTIFIER=${encodeURIComponent(idParam)}${hostParam}&lines=200`;
this.http.get<{ entries: JournalEntry[] }>(url).subscribe({
next: (response) => {
this.diskLogsContent = this.formatEntries(response.entries || []);
this.loadingDiskLogs = false;
this.errorDiskLogs = null;
},
error: () => {
this.errorDiskLogs = 'Error al cargar logs de disco';
this.loadingDiskLogs = false;
}
});
}
private formatEntries(entries: JournalEntry[]): string {
return entries.map(entry => {
const ts = entry.__REALTIME_TIMESTAMP
? new Date(parseInt(entry.__REALTIME_TIMESTAMP, 10) / 1000).toISOString().replace('T', ' ').substring(0, 23)
: ' ';
const id = entry.SYSLOG_IDENTIFIER ?? '';
return `[${ts}] ${id}: ${entry.MESSAGE ?? ''}`;
}).join('\n');
}
private startPolling(): void {
this.stopPolling();
this.pollInterval = setInterval(() => {
if (this.isLive) {
this.loadDiskLogs();
}
}, this.POLL_INTERVAL_MS);
}
private stopPolling(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
refreshLogs(): void {
this.loadDiskLogs();
}
private scrollToBottom(): void {
if (this.isLive && this.logContainer) {
try {
this.logContainer.nativeElement.scrollTop = this.logContainer.nativeElement.scrollHeight;
} catch (err) {}
}
}
}

View File

@ -232,6 +232,10 @@
"clientListPartition": "Llistat de clients on es realitzarà el particionat",
"clientLogs": "Registres de client",
"diskLogs": "Registres de disc",
"oglogLogs": "Registres d'oglog",
"realTimeLogs": "Visualització de registres en temps real",
"diskLogsStartLive": "Activar mode en temps real",
"diskLogsStopLive": "Aturar mode en temps real",
"clientMacOS": "Client MacOS",
"clientOff": "Client apagat",
"clientOgLive": "Client OGLive",
@ -1032,6 +1036,7 @@
"ogbootServers": "Servidors OgBoot",
"ogBootServerStatus": "Estat del servidor OgBoot",
"ogcoreLogs": "Logs d'OgCore",
"ogrepositoryLogs": "Logs d'OgRepository",
"ogCoreServer": "Servidor OgCore",
"ogLive": "ogLive",
"ogLiveDeletedSuccessfully": "OG Live eliminat amb èxit",

View File

@ -232,6 +232,8 @@
"clientListPartition": "List of clients where partitioning will be performed",
"clientLogs": "Client Logs",
"diskLogs": "Disk Logs",
"oglogLogs": "oglog Logs",
"realTimeLogs": "Real-time log visualization",
"diskLogsStartLive": "Enable real-time mode",
"diskLogsStopLive": "Stop real-time mode",
"clientMacOS": "MacOS client",
@ -1034,6 +1036,7 @@
"ogbootServers": "OgBoot Servers",
"ogBootServerStatus": "OgBoot Server Status",
"ogcoreLogs": "OgCore Logs",
"ogrepositoryLogs": "OgRepository Logs",
"ogCoreServer": "OgCore Server",
"ogLive": "ogLive",
"ogLiveDeletedSuccessfully": "OG Live deleted successfully",

View File

@ -232,6 +232,8 @@
"clientListPartition": "Listado de clientes donde se realizará el particionado",
"clientLogs": "Logs de cliente",
"diskLogs": "Logs de disco",
"oglogLogs": "Logs de oglog",
"realTimeLogs": "Visualización de logs en tiempo real",
"diskLogsStartLive": "Activar modo en tiempo real",
"diskLogsStopLive": "Detener modo en tiempo real",
"clientMacOS": "Cliente MacOS",
@ -1034,6 +1036,7 @@
"ogbootServers": "Servidores OgBoot",
"ogBootServerStatus": "Estado del servidor OgBoot",
"ogcoreLogs": "Logs de OgCore",
"ogrepositoryLogs": "Logs de OgRepository",
"ogCoreServer": "Servidor OgCore",
"ogLive": "ogLive",
"ogLiveDeletedSuccessfully": "OG Live eliminado con éxito",