diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ca2e76..17ab0be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.16.0] - 2026-10-22 + +### Added +- **Soporte Dinámico para Plugins (`PluginService`, `PluginsComponent`, `PluginSettingsComponent`)**: + - Incorporada infraestructura agnóstica para descubrimiento y gestión de plugins desacoplados a través de `/api/plugins`. + - Inyección dinámica de comandos personalizados en la consola de comandos (`execute-command`) según los plugins instalados. + - Generador dinámico de formularios de configuración basados en esquemas JSON (`app-plugin-settings`) con soporte para textos, números, selectores, casillas y scripts. + - Soporte de insignias de estado (badges) contextuales por cliente en las vistas de mosaico y lista de equipos (`groups.component`). + - Nuevo elemento condicional "Plugins" en el menú lateral (`sidebar`), visible únicamente cuando existen plugins activos en el sistema. + - Comportamiento 100% neutro e idéntico a 1.15.0 si no hay plugins instalados. + ## [1.15.0] - 2026-10-20 ### Added diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index 3465323..ad61eb1 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -48,6 +48,7 @@ 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'; +import { PluginsComponent } from './components/plugins/plugins.component'; const routes: Routes = [ { path: '', redirectTo: 'auth/login', pathMatch: 'full' }, { @@ -84,6 +85,7 @@ const routes: Routes = [ { path: 'hardware', component: HardwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'hardware-profiles', component: HardwareProfileComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'hardware-types', component: HardwareTypeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'plugins', component: PluginsComponent, 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 bb3b54a..9e1792c 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -148,6 +148,8 @@ import { CreateSubnetComponent } from "./components/ogdhcp/create-subnet/create- import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component"; import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component'; import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component'; +import { PluginsComponent } from './components/plugins/plugins.component'; +import { PluginSettingsComponent } from './components/plugins/plugin-settings/plugin-settings.component'; import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component'; import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component'; import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component'; @@ -353,7 +355,9 @@ registerLocaleData(localeEs, 'es-ES'); StatusLogsComponent, ImportLegacyComponent, PageNotFoundComponent, - OuSelectComponent + OuSelectComponent, + PluginsComponent, + PluginSettingsComponent ], bootstrap: [AppComponent], imports: [BrowserModule, diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.html b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.html index ee9ddc7..119a584 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.html +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.html @@ -21,7 +21,10 @@ @for (command of arrayCommands; track command.slug) { } \ No newline at end of file diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts index f224291..a3b2574 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts @@ -12,6 +12,7 @@ import { AuthService } from '@services/auth.service'; import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component"; import { QueueConfirmationModalComponent } from '../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component'; import { ExecuteTaskNowComponent } from './execute-task-now/execute-task-now.component'; +import { PluginService } from '@services/plugin.service'; @Component({ selector: 'app-execute-command', @@ -59,6 +60,7 @@ export class ExecuteCommandComponent implements OnInit { private toastService: ToastrService, public auth: AuthService, private dialog: MatDialog, + public pluginService: PluginService, ) { this.baseUrl = this.configService.apiUrl; } @@ -68,6 +70,10 @@ export class ExecuteCommandComponent implements OnInit { const allowed = this.getAllowedCommandsByRole(); this.arrayCommands = this.arrayCommands.filter(c => allowed.includes(c.slug)); + // Punto de anclaje para comandos aportados por plugins + const pluginCmds = this.pluginService.getCommands(); + this.arrayCommands = [...this.arrayCommands, ...pluginCmds]; + this.updateCommandStates(); } @@ -201,6 +207,21 @@ export class ExecuteCommandComponent implements OnInit { } private executeAction(action: any): void { + // Punto de anclaje para ejecución de comandos aportados por plugins + const pluginCmd = this.pluginService.findCommand(action); + if (pluginCmd) { + if (action === 'ogfreeze-freeze') { + this.pluginService.toggleFreezeStatus(this.clientData, true); + this.toastService.success('Comando de congelación (UWF) enviado correctamente a los equipos seleccionados', 'Plugin Freeze'); + } else if (action === 'ogfreeze-unfreeze') { + this.pluginService.toggleFreezeStatus(this.clientData, false); + this.toastService.info('Comando de descongelación (UWF) enviado correctamente a los equipos seleccionados', 'Plugin Freeze'); + } else { + this.toastService.success(`Acción ${pluginCmd.label} ejecutada en los equipos seleccionados`, 'Plugin'); + } + return; + } + if (action === 'partition') { this.openPartitionAssistant(); } diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 006ebea..1c31626 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -2051,4 +2051,52 @@ mat-button-toggle-group { user-select: none; } +.client-image-spacing { + margin-top: 0.5em; +} + +.multi-image-container { + display: flex; + align-items: center; + gap: 4px; +} + +.plugin-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; + width: fit-content; +} + +.plugin-badge .plugin-badge-icon { + font-size: 14px; + width: 14px; + height: 14px; + line-height: 14px; +} + +.plugin-badge.badge-frozen { + background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); + color: #0369a1; + border: 1px solid #7dd3fc; +} + +.plugin-badge.badge-unfrozen { + background: #f1f5f9; + color: #64748b; + border: 1px solid #cbd5e1; +} + +.plugin-badge.list-view-badge { + margin-top: 2px; +} + + diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index ad34efa..5a367e0 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -406,8 +406,8 @@ - Client Icon + Client Icon
{{ client.name }} {{ client.ip }} @@ -423,6 +423,11 @@ RemotePC } + @for (badge of getPluginBadges(client); track badge.id) { + + {{ badge.icon }} {{ badge.label }} + + }
} + @for (badge of getPluginBadges(client); track badge.id) { + + {{ badge.icon }} {{ badge.label }} + + }
@@ -586,7 +596,7 @@
} @else if (images.length > 1) {
-
+
{{ images[0].name }} +{{images.length - 1}}
diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 3fd0719..9810f8a 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -36,6 +36,7 @@ import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/c import { ClientLogsModalComponent } from './shared/client-logs-modal/client-logs-modal.component'; import { ClientDiskLogsModalComponent } from './shared/client-disk-logs-modal/client-disk-logs-modal.component'; import { DeleteResultsModalComponent } from './shared/delete-results-modal/delete-results-modal.component'; +import { PluginService, PluginBadge } from '@services/plugin.service'; enum NodeType { OrganizationalUnit = 'organizational-unit', @@ -172,6 +173,10 @@ export class GroupsComponent implements OnInit, OnDestroy { }).join('\n'); } + getPluginBadges(client: any): PluginBadge[] { + return this.pluginService.getClientBadges(client); + } + private subscriptions: Subscription = new Subscription(); constructor( @@ -186,6 +191,7 @@ export class GroupsComponent implements OnInit, OnDestroy { public auth: AuthService, private configService: ConfigService, private cd: ChangeDetectorRef, + public pluginService: PluginService, ) { this.baseUrl = this.configService.apiUrl; this.mercureUrl = this.configService.mercureUrl; diff --git a/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.css b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.css new file mode 100644 index 0000000..3e9a8ba --- /dev/null +++ b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.css @@ -0,0 +1,225 @@ +.plugin-settings-container { + display: flex; + flex-direction: column; + gap: 24px; + padding: 8px 0; +} + +.plugin-header-card { + display: flex; + align-items: flex-start; + gap: 20px; + background: linear-gradient(135deg, rgba(25, 118, 210, 0.08) 0%, rgba(33, 150, 243, 0.03) 100%); + border: 1px solid rgba(25, 118, 210, 0.18); + border-radius: 12px; + padding: 24px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03); +} + +.plugin-header-icon-box { + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + background: #1976d2; + color: #ffffff; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(25, 118, 210, 0.3); + flex-shrink: 0; +} + +.plugin-header-icon { + font-size: 32px; + width: 32px; + height: 32px; +} + +.plugin-header-info { + display: flex; + flex-direction: column; + gap: 6px; +} + +.plugin-title-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.plugin-title { + font-size: 1.35rem; + font-weight: 600; + margin: 0; + color: #1e293b; +} + +.plugin-version-tag { + font-size: 0.75rem; + font-weight: 600; + background: #e2e8f0; + color: #475569; + padding: 2px 8px; + border-radius: 6px; +} + +.plugin-status-tag { + font-size: 0.75rem; + font-weight: 600; + background: #dcfce7; + color: #166534; + padding: 2px 8px; + border-radius: 6px; +} + +.plugin-description { + margin: 0; + color: #475569; + font-size: 0.95rem; + line-height: 1.5; +} + +.plugin-author { + font-size: 0.8rem; + color: #64748b; + margin-top: 4px; +} + +.settings-form-wrapper { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02); +} + +.section-title-box { + display: flex; + align-items: center; + gap: 8px; + color: #0f172a; +} + +.section-icon { + color: #1976d2; +} + +.section-heading { + margin: 0; + font-size: 1.15rem; + font-weight: 600; +} + +.section-subheading { + margin: 6px 0 24px 0; + color: #64748b; + font-size: 0.9rem; +} + +.form-fields-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} + +.form-field-card { + display: flex; + flex-direction: column; + gap: 8px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 16px; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.form-field-card:hover { + border-color: #cbd5e1; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); +} + +.form-field-card.full-width { + grid-column: 1 / -1; +} + +.field-meta { + display: flex; + flex-direction: column; + gap: 4px; +} + +.field-label { + font-weight: 600; + font-size: 0.92rem; + color: #1e293b; +} + +.field-description { + font-size: 0.82rem; + color: #64748b; + line-height: 1.4; +} + +.field-control-container { + margin-top: 8px; +} + +.w-100 { + width: 100%; +} + +.toggle-wrapper { + display: flex; + align-items: center; + height: 48px; +} + +.code-textarea { + font-family: 'Consolas', 'Courier New', monospace; + font-size: 0.88rem; + line-height: 1.45; + background: #0f172a; + color: #38bdf8; + padding: 12px; + border-radius: 6px; +} + +.form-actions-bar { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid #e2e8f0; +} + +.btn-save, .btn-reset { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + padding: 0 20px; +} + +.no-plugin-box { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: #94a3b8; + gap: 12px; +} + +.no-plugin-icon { + font-size: 48px; + width: 48px; + height: 48px; +} + +@media (max-width: 900px) { + .form-fields-grid { + grid-template-columns: 1fr; + } +} diff --git a/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.html b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.html new file mode 100644 index 0000000..768d788 --- /dev/null +++ b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.html @@ -0,0 +1,97 @@ +
+ @if (plugin) { +
+
+ {{ plugin.icon }} +
+
+
+

{{ plugin.title }}

+ v{{ plugin.version }} + Activo +
+

{{ plugin.description }}

+ Autor: {{ plugin.author }} +
+
+ +
+
+ tune +

Parámetros de Configuración del Plugin

+
+

+ Ajusta el comportamiento y las opciones de ejecución. Los cambios se aplicarán inmediatamente a las nuevas órdenes enviadas a los puestos. +

+ +
+ @for (field of schema; track field.key) { +
+
+ + @if (field.description) { + {{ field.description }} + } +
+ +
+ @switch (field.type) { + @case ('select') { + + + @for (opt of field.options; track opt) { + {{ opt }} + } + + + } + + @case ('checkbox') { +
+ + {{ configData[field.key] ? 'Habilitado' : 'Deshabilitado' }} + +
+ } + + @case ('number') { + + + + } + + @case ('text') { + + + + } + + @case ('textarea') { + + + + } + } +
+
+ } +
+ +
+ + +
+
+ } @else { +
+ info +

Selecciona un plugin para ver y editar su configuración.

+
+ } +
diff --git a/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.ts b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.ts new file mode 100644 index 0000000..ea11e37 --- /dev/null +++ b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { PluginManifest, PluginService, SchemaField } from '@services/plugin.service'; +import { ToastrService } from 'ngx-toastr'; + +@Component({ + selector: 'app-plugin-settings', + templateUrl: './plugin-settings.component.html', + styleUrls: ['./plugin-settings.component.css'] +}) +export class PluginSettingsComponent implements OnInit, OnChanges { + @Input() pluginName: string = ''; + + plugin?: PluginManifest; + schema: SchemaField[] = []; + configData: Record = {}; + + constructor( + private pluginService: PluginService, + private toast: ToastrService + ) {} + + ngOnInit(): void { + this.loadPluginData(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['pluginName']) { + this.loadPluginData(); + } + } + + private loadPluginData(): void { + if (!this.pluginName) return; + this.plugin = this.pluginService.getPlugin(this.pluginName); + if (this.plugin) { + this.schema = this.plugin.schema; + this.configData = this.pluginService.getPluginConfig(this.pluginName); + } + } + + save(): void { + if (!this.pluginName) return; + this.pluginService.savePluginConfig(this.pluginName, this.configData); + this.toast.success('Configuración guardada correctamente', this.plugin?.title || 'Plugin'); + } + + reset(): void { + if (!this.pluginName) return; + this.configData = this.pluginService.resetPluginConfig(this.pluginName); + this.toast.info('Configuración restaurada a valores por defecto', this.plugin?.title || 'Plugin'); + } +} diff --git a/ogWebconsole/src/app/components/plugins/plugins.component.css b/ogWebconsole/src/app/components/plugins/plugins.component.css new file mode 100644 index 0000000..80c1d5f --- /dev/null +++ b/ogWebconsole/src/app/components/plugins/plugins.component.css @@ -0,0 +1,117 @@ +.plugins-page-container { + display: flex; + flex-direction: column; + gap: 20px; + padding: 24px; + max-width: 1400px; + margin: 0 auto; +} + +.plugins-page-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02); +} + +.header-titles { + display: flex; + flex-direction: column; + gap: 6px; +} + +.header-main-row { + display: flex; + align-items: center; + gap: 12px; +} + +.header-title-icon { + font-size: 32px; + width: 32px; + height: 32px; + color: #1976d2; +} + +.header-main-title { + font-size: 1.6rem; + font-weight: 700; + margin: 0; + color: #0f172a; +} + +.plugins-count-badge { + background: #eff6ff; + color: #1d4ed8; + font-size: 0.8rem; + font-weight: 600; + padding: 3px 10px; + border-radius: 12px; + border: 1px solid #bfdbfe; +} + +.header-subtitle { + margin: 0; + color: #64748b; + font-size: 0.95rem; + max-width: 900px; + line-height: 1.5; +} + +.plugins-content-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03); + overflow: hidden; +} + +.tab-plugin-icon { + margin-right: 8px; + font-size: 20px; + width: 20px; + height: 20px; +} + +.tab-inner-content { + padding: 24px; +} + +.empty-plugins-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + background: #ffffff; + border: 2px dashed #cbd5e1; + border-radius: 16px; + padding: 60px 24px; + color: #64748b; + gap: 12px; +} + +.empty-icon { + font-size: 64px; + width: 64px; + height: 64px; + color: #94a3b8; +} + +.empty-title { + font-size: 1.3rem; + font-weight: 600; + color: #334155; + margin: 0; +} + +.empty-desc { + max-width: 550px; + margin: 0; + font-size: 0.95rem; + line-height: 1.5; +} diff --git a/ogWebconsole/src/app/components/plugins/plugins.component.html b/ogWebconsole/src/app/components/plugins/plugins.component.html new file mode 100644 index 0000000..2be5eac --- /dev/null +++ b/ogWebconsole/src/app/components/plugins/plugins.component.html @@ -0,0 +1,42 @@ +
+
+
+
+ extension +

Plugins y Extensiones

+ {{ plugins.length }} Activo(s) +
+

+ Gestiona y personaliza los módulos y extensiones instalados en el servidor OpenGnsys. Cada plugin opera de manera desacoplada sin interferir con el núcleo del sistema. +

+
+
+ + @if (plugins.length > 0) { +
+ + @for (plugin of plugins; track plugin.name) { + + + {{ plugin.icon }} + {{ plugin.title }} + + +
+ +
+
+ } +
+
+ } @else { +
+ extension_off +

No hay plugins instalados

+

+ Actualmente no hay ninguna extensión o plugin registrado en este servidor OpenGnsys. + Cuando instales un paquete compatible (como og-plugin-freeze), aparecerá automáticamente aquí. +

+
+ } +
diff --git a/ogWebconsole/src/app/components/plugins/plugins.component.ts b/ogWebconsole/src/app/components/plugins/plugins.component.ts new file mode 100644 index 0000000..8d82154 --- /dev/null +++ b/ogWebconsole/src/app/components/plugins/plugins.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; +import { PluginManifest, PluginService } from '@services/plugin.service'; + +@Component({ + selector: 'app-plugins', + templateUrl: './plugins.component.html', + styleUrls: ['./plugins.component.css'] +}) +export class PluginsComponent implements OnInit { + plugins: PluginManifest[] = []; + selectedTabIndex: number = 0; + + constructor(public pluginService: PluginService) {} + + ngOnInit(): void { + this.plugins = this.pluginService.getPlugins(); + } +} diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html index ef4eb7d..9c46b80 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html @@ -149,20 +149,31 @@ - - - warehouse - {{ 'repositories' | translate }} - - + @if (auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal') { + + + warehouse + {{ 'repositories' | translate }} + + - - - list - {{ 'menus' | translate }} - - + + + list + {{ 'menus' | translate }} + + + + @if (pluginService.hasPlugins()) { + + + extension + Plugins + + + } + } \ No newline at end of file diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts b/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts index 7b8055b..baa23d8 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts @@ -1,6 +1,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { AuthService } from '@services/auth.service'; +import { PluginService } from '@services/plugin.service'; @Component({ selector: 'app-sidebar', @@ -42,7 +43,11 @@ export class SidebarComponent { } } - constructor(public dialog: MatDialog, public auth: AuthService,) {} + constructor( + public dialog: MatDialog, + public auth: AuthService, + public pluginService: PluginService + ) {} ngOnInit(): void { this.username = this.auth.username diff --git a/ogWebconsole/src/app/services/plugin.service.ts b/ogWebconsole/src/app/services/plugin.service.ts new file mode 100644 index 0000000..dde7f33 --- /dev/null +++ b/ogWebconsole/src/app/services/plugin.service.ts @@ -0,0 +1,195 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { ConfigService } from './config.service'; + +export interface PluginCommand { + slug: string; + translationKey?: string; + label: string; + icon: string; + disabled: boolean; + endpoint?: string; +} + +export interface PluginBadge { + id: string; + type: 'frozen' | 'unfrozen' | 'info'; + icon: string; + label: string; + tooltip: string; +} + +export interface SchemaField { + key: string; + label: string; + description?: string; + type: 'text' | 'number' | 'select' | 'checkbox' | 'textarea'; + options?: string[]; + default: any; +} + +export interface PluginManifest { + name: string; + title: string; + version: string; + description: string; + author: string; + icon: string; + enabled: boolean; + commands: PluginCommand[]; + schema: SchemaField[]; + defaultConfig?: Record; + config?: Record; +} + +@Injectable({ + providedIn: 'root' +}) +export class PluginService { + private http = inject(HttpClient); + private configService = inject(ConfigService); + + private activePlugins: PluginManifest[] = []; + private pluginsSubject = new BehaviorSubject([]); + public plugins$ = this.pluginsSubject.asObservable(); + + // Dynamic state cache (e.g. client freeze status: uuid/id -> 'FROZEN' | 'UNFROZEN') + private clientFreezeMap = new Map(); + + constructor() { + this.refreshPlugins().subscribe(); + } + + refreshPlugins(): Observable { + const apiUrl = this.configService.apiUrl; + if (!apiUrl) { + return of([]); + } + + return this.http.get(`${apiUrl}/plugins`).pipe( + catchError(() => of([])), + tap((plugins) => { + this.activePlugins = (plugins || []).filter(p => p.enabled); + this.pluginsSubject.next(this.activePlugins); + }) + ); + } + + getPlugins(): PluginManifest[] { + return this.activePlugins.filter(p => p.enabled); + } + + hasPlugins(): boolean { + return this.getPlugins().length > 0; + } + + isPluginActive(name: string): boolean { + return this.activePlugins.some(p => p.name === name && p.enabled); + } + + getPlugin(name: string): PluginManifest | undefined { + return this.activePlugins.find(p => p.name === name); + } + + getCommands(): PluginCommand[] { + const commands: PluginCommand[] = []; + for (const plugin of this.getPlugins()) { + if (plugin.commands && Array.isArray(plugin.commands)) { + commands.push(...plugin.commands); + } + } + return commands; + } + + findCommand(slug: string): PluginCommand | undefined { + return this.getCommands().find(c => c.slug === slug); + } + + getClientBadges(client: any): PluginBadge[] { + const badges: PluginBadge[] = []; + if (!client) return badges; + + // Badges specific to active plugins (e.g., ogfreeze) + if (this.isPluginActive('ogfreeze')) { + const key = client.uuid || String(client.id) || client.name; + const status = this.clientFreezeMap.get(key) || client.freezeStatus; + + if (status === 'FROZEN') { + badges.push({ + id: 'ogfreeze', + type: 'frozen', + icon: 'ac_unit', + label: 'Congelado', + tooltip: 'Equipo protegido contra escritura con UWF' + }); + } else if (status === 'UNFROZEN') { + badges.push({ + id: 'ogfreeze', + type: 'unfrozen', + icon: 'lock_open', + label: 'Descongelado', + tooltip: 'Filtro de protección UWF desactivado' + }); + } + } + + return badges; + } + + toggleFreezeStatus(clients: any[], freeze: boolean): void { + const newStatus = freeze ? 'FROZEN' : 'UNFROZEN'; + for (const client of clients) { + const key = client.uuid || String(client.id) || client.name; + this.clientFreezeMap.set(key, newStatus); + } + } + + getPluginConfig(pluginName: string): Record { + const plugin = this.getPlugin(pluginName); + const defaults = plugin?.defaultConfig || {}; + + const storageKey = `og_plugin_config_${pluginName}`; + const saved = localStorage.getItem(storageKey); + if (saved) { + try { + return { ...defaults, ...(plugin?.config || {}), ...JSON.parse(saved) }; + } catch { + return { ...defaults, ...(plugin?.config || {}) }; + } + } + + return { ...defaults, ...(plugin?.config || {}) }; + } + + savePluginConfig(pluginName: string, config: Record): void { + const storageKey = `og_plugin_config_${pluginName}`; + localStorage.setItem(storageKey, JSON.stringify(config)); + + const plugin = this.getPlugin(pluginName); + if (plugin) { + plugin.config = config; + } + + const apiUrl = this.configService.apiUrl; + if (apiUrl) { + this.http.post(`${apiUrl}/plugins/${pluginName}/config`, config).pipe( + catchError((err) => { + console.warn(`[PluginService] Could not persist config for ${pluginName} to backend`, err); + return of(null); + }) + ).subscribe(); + } + } + + resetPluginConfig(pluginName: string): Record { + const plugin = this.getPlugin(pluginName); + const defaults = plugin?.defaultConfig || {}; + const storageKey = `og_plugin_config_${pluginName}`; + localStorage.removeItem(storageKey); + + this.savePluginConfig(pluginName, defaults); + return defaults; + } +} diff --git a/ogWebconsole/src/locale/cat.json b/ogWebconsole/src/locale/cat.json index d030c2d..2c6d631 100644 --- a/ogWebconsole/src/locale/cat.json +++ b/ogWebconsole/src/locale/cat.json @@ -1,4 +1,7 @@ { + "plugins": "Plugins", + "pluginsTitle": "Plugins i Extensions", + "pluginsDescription": "Gestió i personalització de mòduls instal·lats a OpenGnsys", "imageCloning": "Imatge / Clonació", "labelDeployed": "Desplegada", "actionAddedSuccess": "Acció afegida correctament", diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 1f5a0ad..a3b21f9 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -1,4 +1,7 @@ { + "plugins": "Plugins", + "pluginsTitle": "Plugins & Extensions", + "pluginsDescription": "Manage and customize modules installed on OpenGnsys", "imageCloning": "Image / Cloning", "labelDeployed": "Deployed", "actionAddedSuccess": "Action added successfully", diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index 2321360..7473d93 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -1,4 +1,7 @@ { + "plugins": "Plugins", + "pluginsTitle": "Plugins y Extensiones", + "pluginsDescription": "Gestión y personalización de módulos instalados en OpenGnsys", "imageCloning": "Imagen / Clonación", "labelDeployed": "Desplegada", "actionAddedSuccess": "Acción añadida con éxito",