diff --git a/CHANGELOG.md b/CHANGELOG.md index e18b615..0a5a451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ - 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. + - Resaltador de sintaxis enriquecido y visor de terminal para scripts de configuración con numeración de líneas, colores semánticos (comentarios en verde itálica, comandos, parámetros, cadenas), botón de copiado rápido y alternancia entre vista coloreada y editor manual. - Comportamiento 100% neutro e idéntico a 1.15.0 si no hay plugins instalados. ### Fixed - **Recarga de página en Plugins (`plugins.component`, `plugin-settings.component`)**: Corregido el problema de sincronización reactiva al refrescar directamente (F5) la vista de `/plugins`, añadiendo un estado de carga visual y suscripción a `plugins$` para evitar mostrar prematuramente la pantalla de "No hay plugins instalados". +- **Rutas excluidas vacías en ogfreeze (`plugin-settings.component`)**: Corregida la regeneración de scripts cuando el campo de rutas excluidas se deja intencionadamente en blanco; ahora omite de forma limpia el bloque de exclusiones en lugar de forzar los valores predeterminados. ## [1.15.0] - 2026-10-20 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 index 4c0ba06..1a56951 100644 --- a/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.css +++ b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.css @@ -181,46 +181,203 @@ height: 48px; } -/* Visor de código para scripts con alto contraste */ -.code-field { +/* Editor de código y visor con resaltado de sintaxis */ +.code-editor-box { margin-top: 4px; + background-color: #0b1120; + border: 1px solid #1e293b; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); } -.code-field .mat-mdc-text-field-wrapper { - background-color: #0b1120 !important; - border-radius: 8px !important; +.code-editor-header { + display: flex; + justify-content: space-between; + align-items: center; + background: #0f172a; + padding: 8px 14px; + border-bottom: 1px solid #1e293b; } -textarea.code-textarea, -.code-field textarea.mat-mdc-input-element { - font-family: 'Consolas', 'Fira Code', 'Monaco', monospace !important; - font-size: 0.92rem !important; - line-height: 1.6 !important; - background-color: #0b1120 !important; - color: #38bdf8 !important; - -webkit-text-fill-color: #38bdf8 !important; - caret-color: #38bdf8 !important; - padding: 14px !important; - border-radius: 6px !important; +.code-header-left { + display: flex; + align-items: center; + gap: 7px; } -.code-field .mdc-notched-outline__leading, -.code-field .mdc-notched-outline__notch, -.code-field .mdc-notched-outline__trailing { - border-color: #334155 !important; +.terminal-dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; } -.code-field:hover .mdc-notched-outline__leading, -.code-field:hover .mdc-notched-outline__notch, -.code-field:hover .mdc-notched-outline__trailing { - border-color: #64748b !important; +.terminal-dot.red { + background-color: #ef4444; } -.code-field.mat-focused .mdc-notched-outline__leading, -.code-field.mat-focused .mdc-notched-outline__notch, -.code-field.mat-focused .mdc-notched-outline__trailing { - border-color: #38bdf8 !important; - border-width: 2px !important; +.terminal-dot.yellow { + background-color: #f59e0b; +} + +.terminal-dot.green { + background-color: #10b981; +} + +.code-lang-badge { + font-family: 'Consolas', 'Fira Code', 'Monaco', monospace; + font-size: 0.72rem; + font-weight: 600; + color: #94a3b8; + background: #1e293b; + padding: 2px 8px; + border-radius: 4px; + margin-left: 8px; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.code-header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.code-action-btn { + display: flex; + align-items: center; + gap: 5px; + background: #1e293b; + color: #94a3b8; + border: 1px solid #334155; + border-radius: 6px; + padding: 4px 10px; + font-size: 0.78rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.code-action-btn:hover { + background: #334155; + color: #f8fafc; + border-color: #475569; +} + +.code-action-btn.active { + background: #0284c7; + color: #ffffff; + border-color: #0284c7; +} + +.action-btn-icon { + font-size: 15px; + width: 15px; + height: 15px; +} + +.code-preview-scroll { + max-height: 280px; + overflow-y: auto; + overflow-x: auto; + padding: 10px 0; + background: #0b1120; +} + +.code-lines-table { + display: flex; + flex-direction: column; + width: 100%; + font-family: 'Consolas', 'Fira Code', 'Monaco', monospace; + font-size: 0.92rem; + line-height: 1.6; +} + +.code-line-row { + display: flex; + width: 100%; + transition: background-color 0.15s; +} + +.code-line-row:hover { + background-color: rgba(255, 255, 255, 0.04); +} + +.line-number { + min-width: 42px; + padding: 0 12px; + text-align: right; + color: #475569; + user-select: none; + border-right: 1px solid #1e293b; + font-size: 0.82rem; +} + +.line-code { + padding: 0 16px; + white-space: pre; + color: #f8fafc; + flex: 1; +} + +/* Tokens de resaltado de sintaxis */ +.syntax-comment { + color: #86efac; + font-style: italic; +} + +.syntax-cmd { + color: #38bdf8; + font-weight: 600; +} + +.syntax-keyword { + color: #c084fc; + font-weight: 500; +} + +.syntax-flag { + color: #f472b6; +} + +.syntax-string { + color: #fde047; +} + +.syntax-val { + color: #34d399; + font-weight: 600; +} + +.syntax-num { + color: #fb923c; +} + +.code-textarea-wrapper { + padding: 10px; + background: #0b1120; +} + +.code-raw-textarea { + width: 100%; + background-color: #090d16; + color: #f8fafc; + border: 1px solid #334155; + border-radius: 6px; + padding: 12px; + font-family: 'Consolas', 'Fira Code', 'Monaco', monospace; + font-size: 0.92rem; + line-height: 1.6; + resize: vertical; + box-sizing: border-box; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.code-raw-textarea:focus { + border-color: #38bdf8; + box-shadow: 0 0 0 1px #38bdf8; } .form-actions-bar { 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 index ce277d5..ad9b387 100644 --- a/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.html +++ b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.html @@ -58,7 +58,7 @@ @switch (field.type) { @case ('select') { - + @for (opt of field.options; track opt) { {{ opt }} } @@ -68,7 +68,7 @@ @case ('checkbox') {
- + {{ configData[field.key] ? 'Habilitado' : 'Deshabilitado' }}
@@ -76,20 +76,61 @@ @case ('number') { - + } @case ('text') { - + } @case ('textarea') { - - - +
+
+
+ + + + PowerShell +
+
+ + +
+
+ + @if (!isEditingScript(field.key)) { +
+
+ @for (line of getScriptLines(configData[field.key]); track $index) { +
+ {{ $index + 1 }} + +
+ } +
+
+ } @else { +
+ +
+ } +
} } 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 index 552ae19..b1f657f 100644 --- a/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.ts +++ b/ogWebconsole/src/app/components/plugins/plugin-settings/plugin-settings.component.ts @@ -1,6 +1,7 @@ -import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core'; import { PluginManifest, PluginService, SchemaField } from '@services/plugin.service'; import { ToastrService } from 'ngx-toastr'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { Subscription } from 'rxjs'; @Component({ @@ -14,7 +15,11 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy { plugin?: PluginManifest; schema: SchemaField[] = []; configData: Record = {}; + editingScripts: Record = {}; + hasManualScriptEdits: boolean = false; + private pluginSub?: Subscription; + private sanitizer = inject(DomSanitizer); constructor( private pluginService: PluginService, @@ -51,41 +56,147 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy { return this.schema.some(f => f.key === 'freeze_script' || f.key === 'unfreeze_script'); } - regenerateScripts(): void { + onParamChange(): void { + if (!this.hasManualScriptEdits) { + this.regenerateScripts(false); + } + } + + onManualScriptInput(key: string): void { + this.hasManualScriptEdits = true; + } + + isEditingScript(key: string): boolean { + return !!this.editingScripts[key]; + } + + toggleScriptEdit(key: string): void { + this.editingScripts[key] = !this.editingScripts[key]; + } + + copyToClipboard(text: string): void { + if (!text) return; + navigator.clipboard.writeText(text).then(() => { + this.toast.success('Script copiado al portapapeles', 'Portapapeles'); + }).catch(() => { + this.toast.error('No se pudo copiar el script al portapapeles', 'Error'); + }); + } + + getScriptLines(script: string): SafeHtml[] { + if (!script) return []; + const lines = script.split('\n'); + return lines.map(line => this.highlightLine(line)); + } + + private escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>'); + } + + private highlightLine(rawLine: string): SafeHtml { + const trimmed = rawLine.trim(); + + if (trimmed.length === 0) { + return this.sanitizer.bypassSecurityTrustHtml(' '); + } + + // Comentario completo + if (trimmed.startsWith('#')) { + const escaped = this.escapeHtml(rawLine); + return this.sanitizer.bypassSecurityTrustHtml(`${escaped}`); + } + + let escaped = this.escapeHtml(rawLine); + + // 1. Cadenas entre comillas dobles o simples + const strings: string[] = []; + escaped = escaped.replace(/(["'])(.*?)\1/g, (match) => { + strings.push(match); + return `___STR_${strings.length - 1}___`; + }); + + // 2. Parámetros / flags (/r, /t, -c, etc.) + escaped = escaped.replace(/(\s)([\/-][a-zA-Z0-9?]+)/g, '$1$2'); + + // 3. Comandos ejecutables + escaped = escaped.replace(/\b(uwfmgr\.exe|shutdown|powershell\.exe|cmd\.exe)\b/gi, '$1'); + + // 4. Subcomandos y palabras clave + escaped = escaped.replace(/\b(overlay|filter|volume|file|enable|disable|protect|set-type|set-size|add-exclusion)\b/gi, '$1'); + + // 5. Valores y tipos + escaped = escaped.replace(/\b(RAM|DISK|true|false)\b/gi, '$1'); + + // 6. Números + escaped = escaped.replace(/\b(\d+)\b/g, '$1'); + + // Restaurar cadenas + escaped = escaped.replace(/___STR_(\d+)___/g, (_, index) => { + const original = strings[Number(index)]; + return `${original}`; + }); + + return this.sanitizer.bypassSecurityTrustHtml(escaped); + } + + regenerateScripts(showToast: boolean = true): 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`; + // Obtener exclusiones de forma estricta (no usar fallback si el usuario las borró) + const rawExcl = this.configData['exclusions']; + const exclusionsRaw = (rawExcl !== undefined && rawExcl !== null) ? String(rawExcl).trim() : ''; + const exclusions = exclusionsRaw.length > 0 + ? exclusionsRaw.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0) + : []; + + let freezeScript = ''; + let step = 1; + + // Solo incluir el paso de exclusiones si el usuario ha especificado rutas + if (exclusions.length > 0) { + freezeScript += `# ${step}. Excluir metadatos y carpetas del filtro UWF\n`; + for (const excl of exclusions) { + freezeScript += `uwfmgr.exe file add-exclusion "${excl}"\n`; + } + freezeScript += `\n`; + step++; } - freezeScript += `\n# 2. Configurar buffer temporal de escritura\n`; + + freezeScript += `# ${step}. Configurar buffer temporal de escritura\n`; freezeScript += `uwfmgr.exe overlay set-type ${overlayType}\n`; freezeScript += `uwfmgr.exe overlay set-size ${overlaySize}\n\n`; + step++; - freezeScript += `# 3. Proteger volumen y habilitar filtro\n`; + freezeScript += `# ${step}. 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`; + step++; + + freezeScript += `# ${step}. 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'); + this.hasManualScriptEdits = false; + + if (showToast) { + 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'); + if (showToast) { + this.toast.info('Scripts actualizados con los parámetros predeterminados', this.plugin?.title || 'Plugin'); + } } } @@ -98,6 +209,7 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy { reset(): void { if (!this.pluginName) return; this.configData = this.pluginService.resetPluginConfig(this.pluginName); + this.hasManualScriptEdits = false; this.toast.info('Configuración restaurada a valores por defecto', this.plugin?.title || 'Plugin'); } }