diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ced925..7924f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/.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 diff --git a/Jenkins/Jenkinsfile-deb-pkg b/Jenkins/Jenkinsfile-deb-pkg index ad33e30..6bc7418 100644 --- a/Jenkins/Jenkinsfile-deb-pkg +++ b/Jenkins/Jenkinsfile-deb-pkg @@ -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') diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index d6191ac..0af94eb 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -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 ], 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 index 3e91d23..b44e5f0 100644 --- 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 @@ -42,15 +42,13 @@ -
-
-

{{ 'ogbootLogs' | translate }}

-

{{ 'realTimeOgbootLogs' | translate }}

-
-
- -
+
+ +
} 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 index 6a1082b..8abd633 100644 --- 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 @@ -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 : ''; 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 index d051185..f7358c6 100644 --- 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 @@ -1,14 +1,12 @@
-
-
-

{{ 'ogcoreLogs' | translate }}

-

{{ 'realTimeOgcoreLogs' | translate }}

-
-
- -
+
+ +
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 index 38bf92b..d344c9b 100644 --- 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 @@ -42,15 +42,13 @@
-
-
-

{{ 'dhcpLogs' | translate }}

-

{{ 'realTimeDhcpLogs' | translate }}

-
-
- -
+
+ +
} 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 index c15f1a5..9ab253f 100644 --- 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 @@ -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 : ''; 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 index d35736d..a0fae1e 100644 --- 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 @@ -49,16 +49,13 @@ -
-
-

Datos del repositorio: {{ getSelectedRepositoryName() }}

-

{{ 'specificRepoInfo' | translate }}

-
-
- -
+
+ +
} 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 index 32b9bfb..6f06dce 100644 --- 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 @@ -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 : ''; diff --git a/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.css b/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.css new file mode 100644 index 0000000..01ac002 --- /dev/null +++ b/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.css @@ -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; +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.html b/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.html new file mode 100644 index 0000000..d1145ee --- /dev/null +++ b/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.html @@ -0,0 +1,59 @@ +
+
+

{{ title | translate }}

+

{{ 'realTimeLogs' | translate }}

+
+ + + +
+
+
+ + +
+
+ + LIVE +
+
+ + @if (loadingDiskLogs && !diskLogsContent) { +
+ +
+ } + + @if (errorDiskLogs) { +
+ error_outline +

{{ errorDiskLogs }}

+ +
+ } + +
{{ diskLogsContent }}
+
+
+ + +
+ @if (oglogUrl) { + + } @else { +
+ info_outline +

oglog no está disponible o no se ha configurado la URL.

+
+ } +
+
+
+
diff --git a/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.ts b/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.ts new file mode 100644 index 0000000..9053dd4 --- /dev/null +++ b/ogWebconsole/src/app/components/global-status/status-logs/status-logs.component.ts @@ -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) {} + } + } +} diff --git a/ogWebconsole/src/locale/cat.json b/ogWebconsole/src/locale/cat.json index 4f45d79..11983ce 100644 --- a/ogWebconsole/src/locale/cat.json +++ b/ogWebconsole/src/locale/cat.json @@ -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", diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 11ce961..34ec0ce 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -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", diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index ffd8fe7..30a0ef5 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -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",