@@ -5,6 +5,7 @@ import {UtmModuleGroupConfService} from '../../shared/services/utm-module-group-
55import { UtmModuleGroupConfType } from '../../shared/type/utm-module-group-conf.type' ;
66import { UtmToastService } from '../../../shared/alert/utm-toast.service' ;
77import { ModuleChangeStatusBehavior } from '../../shared/behavior/module-change-status.behavior' ;
8+ import { finalize } from 'rxjs/operators' ;
89
910interface ProviderConfig {
1011 id : string ;
@@ -52,6 +53,10 @@ export class GuideSocAiComponent implements OnInit {
5253 private rawConfigs : UtmModuleGroupConfType [ ] = [ ] ;
5354 private groupId : number ;
5455
56+ // Original masked customHeaders value (if backend returned an opaque mask like "*****")
57+ private originalMaskedCustomHeaders : string | null = null ;
58+ private readonly maskedDisplay = '***' ;
59+
5560 providers : ProviderConfig [ ] = [
5661 {
5762 id : 'openai' ,
@@ -297,7 +302,12 @@ export class GuideSocAiComponent implements OnInit {
297302
298303 private loadConfig ( ) {
299304 this . loading = true ;
300- this . moduleGroupService . query ( { moduleId : this . integrationId } ) . subscribe ( response => {
305+ this . moduleGroupService . query ( { moduleId : this . integrationId } ) . pipe (
306+ finalize ( ( ) => {
307+ this . loading = false ;
308+ this . cdr . detectChanges ( ) ;
309+ } )
310+ ) . subscribe ( response => {
301311 const groups = response . body || [ ] ;
302312 if ( groups . length > 0 ) {
303313 this . groupId = groups [ 0 ] . id ;
@@ -344,6 +354,7 @@ export class GuideSocAiComponent implements OnInit {
344354 'changeAlertStatus' : changeStatus ? changeStatus . confValue : 'false' ,
345355 } ;
346356 this . customModelValue = '' ;
357+ this . originalMaskedCustomHeaders = null ;
347358
348359 // Only load provider-specific values if viewing the saved provider
349360 if ( isCurrentSavedProvider ) {
@@ -373,16 +384,25 @@ export class GuideSocAiComponent implements OnInit {
373384 // Check if API key exists in custom headers — show masked if so
374385 const customHeaders = this . getConf ( 'utmstack.socai.customHeaders' ) ;
375386 if ( customHeaders && customHeaders . confValue && customHeaders . confValue !== '{}' ) {
376- try {
377- const headers = JSON . parse ( customHeaders . confValue ) ;
378- const authConfig = this . providerAuthHeaders [ this . activeProvider ] ;
379- if ( authConfig && headers [ authConfig . headerName ] ) {
380- // API key exists — show masked, don't expose the real value
381- this . formValues [ 'apiKey' ] = '*****' ;
382- }
383- } catch ( e ) { }
384- this . formValues [ 'customHeaders' ] = customHeaders . confValue ;
385- this . parseHeadersFromJson ( customHeaders . confValue ) ;
387+ const raw = customHeaders . confValue ;
388+ if ( this . isMaskedValue ( raw ) ) {
389+ // Backend returned the whole confValue masked — preserve original for save
390+ this . originalMaskedCustomHeaders = raw ;
391+ this . formValues [ 'customHeaders' ] = raw ;
392+ this . formValues [ 'apiKey' ] = this . maskedDisplay ;
393+ this . headerRows = [ { key : this . maskedDisplay , value : this . maskedDisplay } ] ;
394+ } else {
395+ try {
396+ const headers = JSON . parse ( raw ) ;
397+ const authConfig = this . providerAuthHeaders [ this . activeProvider ] ;
398+ if ( authConfig && headers [ authConfig . headerName ] ) {
399+ // API key exists — show masked, don't expose the real value
400+ this . formValues [ 'apiKey' ] = this . maskedDisplay ;
401+ }
402+ } catch ( e ) { }
403+ this . formValues [ 'customHeaders' ] = raw ;
404+ this . parseHeadersFromJson ( raw ) ;
405+ }
386406 }
387407
388408 const maxTokensConf = this . getConf ( 'utmstack.socai.maxTokens' ) ;
@@ -422,46 +442,56 @@ export class GuideSocAiComponent implements OnInit {
422442 const changes : UtmModuleGroupConfType [ ] = [ ] ;
423443
424444 // Set provider
425- this . pushChange ( changes , 'utmstack.socai.provider' , this . activeProvider ) ;
445+ this . pushChange ( changes , 'utmstack.socai.provider' , this . activeProvider , 'text' ) ;
426446
427447 // Set model
428- this . pushChange ( changes , 'utmstack.socai.model' , this . getModelValue ( ) ) ;
448+ this . pushChange ( changes , 'utmstack.socai.model' , this . getModelValue ( ) , 'text' ) ;
429449
430450 // Set URL for providers that need it (azure, ollama, custom)
431451 if ( this . formValues [ 'url' ] ) {
432- this . pushChange ( changes , 'utmstack.socai.url' , this . formValues [ 'url' ] ) ;
452+ this . pushChange ( changes , 'utmstack.socai.url' , this . formValues [ 'url' ] , 'text' ) ;
433453 }
434454
435455 // Set maxTokens
436456 if ( this . formValues [ 'maxTokens' ] ) {
437- this . pushChange ( changes , 'utmstack.socai.maxTokens' , this . formValues [ 'maxTokens' ] ) ;
457+ this . pushChange ( changes , 'utmstack.socai.maxTokens' , this . formValues [ 'maxTokens' ] , 'text' ) ;
438458 }
439459
440460 // Set behavior toggles
441- this . pushChange ( changes , 'utmstack.socai.autoAnalyze' , this . formValues [ 'autoAnalyze' ] || 'false' ) ;
442- this . pushChange ( changes , 'utmstack.socai.incidentCreation' , this . formValues [ 'incidentCreation' ] || 'false' ) ;
443- this . pushChange ( changes , 'utmstack.socai.changeAlertStatus' , this . formValues [ 'changeAlertStatus' ] || 'false' ) ;
461+ this . pushChange ( changes , 'utmstack.socai.autoAnalyze' , this . formValues [ 'autoAnalyze' ] || 'false' , 'text' ) ;
462+ this . pushChange ( changes , 'utmstack.socai.incidentCreation' , this . formValues [ 'incidentCreation' ] || 'false' , 'text' ) ;
463+ this . pushChange ( changes , 'utmstack.socai.changeAlertStatus' , this . formValues [ 'changeAlertStatus' ] || 'false' , 'text' ) ;
444464
445465 // Build auth headers
446466 if ( this . activeProvider === 'custom' ) {
447467 // Custom provider: user manages auth type and headers directly
448- this . pushChange ( changes , 'utmstack.socai.authType' , this . formValues [ 'authType' ] || 'custom-headers' ) ;
449- this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . formValues [ 'customHeaders' ] || '{}' ) ;
468+ this . pushChange ( changes , 'utmstack.socai.authType' , this . formValues [ 'authType' ] || 'custom-headers' , 'text' ) ;
469+ if ( this . originalMaskedCustomHeaders && this . hasMaskedHeaderRows ( ) ) {
470+ // User didn't replace the masked rows — preserve original masked value
471+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . originalMaskedCustomHeaders , 'password' ) ;
472+ } else {
473+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . formValues [ 'customHeaders' ] || '{}' , 'password' ) ;
474+ }
450475 } else if ( this . activeProvider === 'ollama' ) {
451476 // Ollama: no auth needed
452- this . pushChange ( changes , 'utmstack.socai.authType' , 'none' ) ;
453- this . pushChange ( changes , 'utmstack.socai.customHeaders' , '{}' ) ;
477+ this . pushChange ( changes , 'utmstack.socai.authType' , 'none' , 'text' ) ;
478+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , '{}' , 'password' ) ;
454479 } else {
455480 // Known providers: build auth header from API key
456481 const authConfig = this . providerAuthHeaders [ this . activeProvider ] ;
457- if ( authConfig && this . formValues [ 'apiKey' ] && this . formValues [ 'apiKey' ] !== '*****' ) {
482+ const apiKey = this . formValues [ 'apiKey' ] ;
483+ if ( authConfig && apiKey && ! this . isMaskedValue ( apiKey ) ) {
458484 // User entered a new API key — build auth headers
459485 const headers : { [ k : string ] : string } = { } ;
460- headers [ authConfig . headerName ] = authConfig . headerValuePrefix + this . formValues [ 'apiKey' ] ;
461- this . pushChange ( changes , 'utmstack.socai.authType' , 'custom-headers' ) ;
462- this . pushChange ( changes , 'utmstack.socai.customHeaders' , JSON . stringify ( headers ) ) ;
486+ headers [ authConfig . headerName ] = authConfig . headerValuePrefix + apiKey ;
487+ this . pushChange ( changes , 'utmstack.socai.authType' , 'custom-headers' , 'text' ) ;
488+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , JSON . stringify ( headers ) , 'password' ) ;
489+ } else if ( this . originalMaskedCustomHeaders && apiKey && this . isMaskedValue ( apiKey ) ) {
490+ // User didn't change the masked API key — preserve original masked value
491+ this . pushChange ( changes , 'utmstack.socai.authType' , 'custom-headers' , 'text' ) ;
492+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . originalMaskedCustomHeaders , 'password' ) ;
463493 }
464- // If apiKey is '*****', don't touch customHeaders — keep existing value in DB
494+ // Otherwise: don't touch customHeaders — keep existing value in DB
465495 }
466496
467497 this . moduleGroupConfService . update ( {
@@ -486,7 +516,12 @@ export class GuideSocAiComponent implements OnInit {
486516 ) ;
487517 }
488518
489- private pushChange ( changes : UtmModuleGroupConfType [ ] , confKey : string , value : string ) {
519+ private pushChange (
520+ changes : UtmModuleGroupConfType [ ] ,
521+ confKey : string ,
522+ value : string ,
523+ confDataType : 'list' | 'password' | 'file' | 'bool' | 'select' | 'text' = 'text'
524+ ) {
490525 const existing = this . getConf ( confKey ) ;
491526 if ( existing ) {
492527 changes . push ( {
@@ -495,6 +530,19 @@ export class GuideSocAiComponent implements OnInit {
495530 confOptions : existing . confOptions ? JSON . stringify ( existing . confOptions ) : existing . confOptions ,
496531 confVisibility : existing . confVisibility ? JSON . stringify ( existing . confVisibility ) : existing . confVisibility ,
497532 } ) ;
533+ } else {
534+ changes . push ( {
535+ id : undefined ,
536+ groupId : this . groupId ,
537+ confKey,
538+ confValue : value ,
539+ confName : confKey . split ( '.' ) [ 2 ] || confKey ,
540+ confDataType,
541+ confDescription : confKey ,
542+ confRequired : true ,
543+ confOptions : undefined ,
544+ confVisibility : undefined ,
545+ } ) ;
498546 }
499547 }
500548
@@ -543,6 +591,14 @@ export class GuideSocAiComponent implements OnInit {
543591 this . formValues [ 'customHeaders' ] = JSON . stringify ( obj ) ;
544592 }
545593
594+ private isMaskedValue ( value : string ) : boolean {
595+ return ! ! value && / ^ \* + $ / . test ( value ) ;
596+ }
597+
598+ private hasMaskedHeaderRows ( ) : boolean {
599+ return this . headerRows . some ( r => this . isMaskedValue ( r . key ) || this . isMaskedValue ( r . value ) ) ;
600+ }
601+
546602 private parseHeadersFromJson ( json : string ) {
547603 this . headerRows = [ ] ;
548604 try {
0 commit comments