feat(plugins): enhance plugin command grouping, OS compatibility check, and script generation
oggui-debian-package/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/tag This commit looks good Details

main
Juan Manuel Bardallo 2026-10-22 10:12:42 +02:00
parent 056c89ea53
commit e5a83a05cd
7 changed files with 125 additions and 6 deletions

View File

@ -5,8 +5,9 @@
### 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.
- Agrupación ordenada de comandos de plugins en submenús dedicados en la consola de comandos (`execute-command`), organizados por plugin para mantener el menú limpio.
- Comprobación y filtrado dinámico de compatibilidad por Sistema Operativo (`os: ['windows']`), deshabilitando automáticamente los comandos en puestos con sistemas incompatibles.
- Generador dinámico de formularios de configuración basados en esquemas JSON (`app-plugin-settings`) con botón de autogeneración y actualización reactiva de scripts según los parámetros seleccionados.
- 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.

View File

@ -27,4 +27,27 @@
<span>{{ command.translationKey ? (command.translationKey | translate) : (command.label || command.slug) }}</span>
</button>
}
@if (pluginGroups.length > 0) {
<mat-divider></mat-divider>
@for (group of pluginGroups; track group.plugin.name) {
<button mat-menu-item [matMenuTriggerFor]="pluginSubMenu">
@if (group.plugin.icon) {
<mat-icon>{{ group.plugin.icon }}</mat-icon>
}
<span>{{ group.plugin.title }}</span>
</button>
<mat-menu #pluginSubMenu="matMenu">
@for (cmd of group.commands; track cmd.slug) {
<button mat-menu-item [disabled]="cmd.disabled" (click)="onCommandSelect(cmd.slug)">
@if (cmd.icon) {
<mat-icon>{{ cmd.icon }}</mat-icon>
}
<span>{{ cmd.label }}</span>
</button>
}
</mat-menu>
}
}
</mat-menu>

View File

@ -65,14 +65,15 @@ export class ExecuteCommandComponent implements OnInit {
this.baseUrl = this.configService.apiUrl;
}
pluginGroups: any[] = [];
ngOnInit(): void {
this.clientData = this.clientData || [];
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];
// Punto de anclaje para grupos de comandos aportados por plugins
this.pluginGroups = this.pluginService.getPluginGroups();
this.updateCommandStates();
}
@ -162,6 +163,14 @@ export class ExecuteCommandComponent implements OnInit {
return command;
});
if (this.pluginGroups && this.pluginGroups.length > 0) {
for (const group of this.pluginGroups) {
for (const cmd of group.commands) {
cmd.disabled = !hasClients || !this.pluginService.isCommandSupportedForClients(cmd, this.clientData);
}
}
}
}
onCommandSelect(action: any): void {

View File

@ -194,7 +194,7 @@
border-top: 1px solid #e2e8f0;
}
.btn-save, .btn-reset {
.btn-save, .btn-reset, .btn-regenerate {
display: flex;
align-items: center;
gap: 8px;

View File

@ -82,6 +82,12 @@
<mat-icon>restart_alt</mat-icon>
Restaurar por defecto
</button>
@if (hasScriptFields()) {
<button mat-stroked-button color="accent" (click)="regenerateScripts()" class="btn-regenerate">
<mat-icon>auto_fix_high</mat-icon>
Regenerar Scripts desde Parámetros
</button>
}
<button mat-raised-button color="primary" (click)="save()" class="btn-save">
<mat-icon>save</mat-icon>
Guardar Configuración

View File

@ -38,6 +38,48 @@ export class PluginSettingsComponent implements OnInit, OnChanges {
}
}
hasScriptFields(): boolean {
return this.schema.some(f => f.key === 'freeze_script' || f.key === 'unfreeze_script');
}
regenerateScripts(): void {
if (!this.pluginName) return;
if (this.pluginName === 'ogfreeze') {
const overlayType = this.configData['overlay_type'] || 'RAM';
const overlaySize = this.configData['overlay_size'] || 2048;
const protectC = this.configData['protect_c'] !== false;
const exclusionsRaw = this.configData['exclusions'] || 'C:\\.ogimage, C:\\OpenGnsys';
const exclusions = exclusionsRaw
.split(',')
.map((s: string) => s.trim())
.filter((s: string) => s.length > 0);
let freezeScript = `# 1. Excluir metadatos y carpetas del filtro UWF\n`;
for (const excl of exclusions) {
freezeScript += `uwfmgr.exe file add-exclusion "${excl}"\n`;
}
freezeScript += `\n# 2. Configurar buffer temporal de escritura\n`;
freezeScript += `uwfmgr.exe overlay set-type ${overlayType}\n`;
freezeScript += `uwfmgr.exe overlay set-size ${overlaySize}\n\n`;
freezeScript += `# 3. Proteger volumen y habilitar filtro\n`;
if (protectC) {
freezeScript += `uwfmgr.exe volume protect C:\n`;
}
freezeScript += `uwfmgr.exe filter enable\n\n`;
freezeScript += `# 4. Reinicio obligatorio para que entre en vigor\n`;
freezeScript += `shutdown /r /t 5 -c "OpenGnsys: Reiniciando para aplicar congelacion de sistema"`;
this.configData['freeze_script'] = freezeScript;
this.configData['unfreeze_script'] = `# Desactivar filtro de protección UWF en el próximo inicio\nuwfmgr.exe filter disable\n\n# Reiniciar para aplicar cambios\nshutdown /r /t 5 -c "OpenGnsys: Reiniciando para descongelar sistema"`;
this.toast.info('Scripts de PowerShell generados a partir de los parámetros actuales', this.plugin?.title || 'Plugin');
} else {
this.toast.info('Scripts actualizados con los parámetros predeterminados', this.plugin?.title || 'Plugin');
}
}
save(): void {
if (!this.pluginName) return;
this.pluginService.savePluginConfig(this.pluginName, this.configData);

View File

@ -11,6 +11,7 @@ export interface PluginCommand {
icon: string;
disabled: boolean;
endpoint?: string;
os?: string[]; // Ej: ['windows'], ['linux']
}
export interface PluginBadge {
@ -44,6 +45,11 @@ export interface PluginManifest {
config?: Record<string, any>;
}
export interface PluginGroup {
plugin: PluginManifest;
commands: PluginCommand[];
}
@Injectable({
providedIn: 'root'
})
@ -93,6 +99,15 @@ export class PluginService {
return this.activePlugins.find(p => p.name === name);
}
getPluginGroups(): PluginGroup[] {
return this.getPlugins()
.filter(p => p.commands && p.commands.length > 0)
.map(p => ({
plugin: p,
commands: p.commands
}));
}
getCommands(): PluginCommand[] {
const commands: PluginCommand[] = [];
for (const plugin of this.getPlugins()) {
@ -107,6 +122,29 @@ export class PluginService {
return this.getCommands().find(c => c.slug === slug);
}
isCommandSupportedForClients(command: PluginCommand, clients: any[]): boolean {
if (!command.os || command.os.length === 0) {
return true;
}
if (!clients || clients.length === 0) {
return true;
}
const requiredOsList = command.os.map(os => os.toLowerCase());
return clients.every(client => {
const statusStr = (client.status || '').toLowerCase();
const matchesStatus = requiredOsList.some(req => statusStr.includes(req));
const hasMatchingPartition = (client.partitions || []).some((partition: any) => {
const osName = (partition.operativeSystem?.name || partition.operativeSystem || '').toLowerCase();
return requiredOsList.some(req => osName.includes(req));
});
return matchesStatus || hasMatchingPartition;
});
}
getClientBadges(client: any): PluginBadge[] {
const badges: PluginBadge[] = [];
if (!client) return badges;