2025-11-24 01:31:15 +00:00
import { Component , OnInit , OnDestroy , inject , signal } from '@angular/core' ;
2025-11-18 00:03:24 +00:00
import { CommonModule } from '@angular/common' ;
2025-11-24 01:31:15 +00:00
import { FormsModule } from '@angular/forms' ;
import { ActivatedRoute , Router , RouterLink } from '@angular/router' ;
2025-11-18 00:03:24 +00:00
import { ApiService , Service } from '../../core/services/api.service' ;
2025-11-24 01:31:15 +00:00
import { ToastService } from '../../core/services/toast.service' ;
interface EnvVar {
key : string ;
value : string ;
}
interface Domain {
domain : string ;
dnsProvider : 'cloudflare' | 'manual' | null ;
isObsolete : boolean ;
}
2025-11-18 00:03:24 +00:00
@Component ( {
selector : 'app-service-detail' ,
standalone : true ,
2025-11-24 01:31:15 +00:00
imports : [ CommonModule , FormsModule , RouterLink ] ,
2025-11-18 00:03:24 +00:00
template : `
< div class = "px-4 sm:px-0" >
@if ( loading ( ) ) {
< div class = "text-center py-12" >
< div class = "inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" > < / div >
< / div >
} @else if ( service ( ) ) {
< div class = "mb-8" >
< div class = "flex items-center justify-between" >
< h1 class = "text-3xl font-bold text-gray-900" > { { service ( ) ! . name } } < / h1 >
< span [ ngClass ] = " {
'badge-success' : service ( ) ! . status === 'running' ,
'badge-danger' : service ( ) ! . status === 'stopped' || service ( ) ! . status === 'failed' ,
'badge-warning' : service ( ) ! . status === 'starting' || service ( ) ! . status === 'stopping'
} " class=" badge text - lg " >
{ { service ( ) ! . status } }
< / span >
< / div >
< / div >
<!-- Details Card -->
< div class = "card mb-6" >
2025-11-24 01:31:15 +00:00
< div class = "flex items-center justify-between mb-4" >
< h2 class = "text-lg font-medium text-gray-900" > Service Details < / h2 >
@if ( ! isEditing ( ) ) {
< button ( click ) = "startEditing()" class = "btn btn-secondary text-sm" >
Edit Service
< / button >
}
< / div >
@if ( ! isEditing ( ) ) {
< dl class = "grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2" >
2025-11-18 00:03:24 +00:00
< div >
2025-11-24 01:31:15 +00:00
< dt class = "text-sm font-medium text-gray-500" > Image < / dt >
< dd class = "mt-1 text-sm text-gray-900" > { { service ( ) ! . image } } < / dd >
2025-11-18 00:03:24 +00:00
< / div >
< div >
2025-11-24 01:31:15 +00:00
< dt class = "text-sm font-medium text-gray-500" > Port < / dt >
< dd class = "mt-1 text-sm text-gray-900" > { { service ( ) ! . port } } < / dd >
< / div >
@if ( service ( ) ! . domain ) {
< div >
< dt class = "text-sm font-medium text-gray-500" > Domain < / dt >
< dd class = "mt-1 text-sm text-gray-900" >
< a [ href ] = "'https://' + service()!.domain" target = "_blank" class = "text-primary-600 hover:text-primary-900" >
{ { service ( ) ! . domain } }
< / a >
< / dd >
< / div >
}
@if ( service ( ) ! . containerID ) {
< div >
< dt class = "text-sm font-medium text-gray-500" > Container ID < / dt >
< dd class = "mt-1 text-sm text-gray-900 font-mono" > { { service ( ) ! . containerID ? . substring ( 0 , 12 ) } } < / dd >
< / div >
}
< div >
< dt class = "text-sm font-medium text-gray-500" > Created < / dt >
< dd class = "mt-1 text-sm text-gray-900" > { { formatDate ( service ( ) ! . createdAt ) } } < / dd >
< / div >
< div >
< dt class = "text-sm font-medium text-gray-500" > Updated < / dt >
< dd class = "mt-1 text-sm text-gray-900" > { { formatDate ( service ( ) ! . updatedAt ) } } < / dd >
< / div >
< / dl >
<!-- Registry Information -->
@if ( service ( ) ! . useOneboxRegistry ) {
< div class = "mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4" >
< h3 class = "text-sm font-semibold text-blue-900 mb-3" > Onebox Registry < / h3 >
< dl class = "grid grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2" >
< div >
< dt class = "text-sm font-medium text-blue-700" > Repository < / dt >
< dd class = "mt-1 text-sm text-blue-900 font-mono" > { { service ( ) ! . registryRepository } } < / dd >
< / div >
< div >
< dt class = "text-sm font-medium text-blue-700" > Tag < / dt >
< dd class = "mt-1 text-sm text-blue-900" > { { service ( ) ! . registryImageTag || 'latest' } } < / dd >
< / div >
@if ( service ( ) ! . registryToken ) {
< div class = "sm:col-span-2" >
< dt class = "text-sm font-medium text-blue-700" > Push / Pull Token < / dt >
< dd class = "mt-1" >
< div class = "flex items-center gap-2" >
< input
type = "password"
[ value ] = "service()!.registryToken"
readonly
class = "input text-xs font-mono flex-1"
# tokenInput
/ >
< button
type = "button"
( click ) = "copyToken(tokenInput.value)"
class = "btn btn-secondary text-xs"
>
Copy
< / button >
< / div >
< p class = "mt-1 text-xs text-blue-600" >
Use this token to push images : < code class = "bg-blue-100 px-1 py-0.5 rounded" > docker login - u unused - p [ token ] { { registryBaseUrl ( ) } } < / code >
< / p >
< / dd >
< / div >
}
< div >
< dt class = "text-sm font-medium text-blue-700" > Auto - update < / dt >
< dd class = "mt-1 text-sm text-blue-900" >
{ { service ( ) ! . autoUpdateOnPush ? 'Enabled' : 'Disabled' } }
< / dd >
< / div >
@if ( service ( ) ! . imageDigest ) {
< div class = "sm:col-span-2" >
< dt class = "text-sm font-medium text-blue-700" > Current Digest < / dt >
< dd class = "mt-1 text-xs text-blue-900 font-mono break-all" > { { service ( ) ! . imageDigest } } < / dd >
< / div >
}
< / dl >
2025-11-18 00:03:24 +00:00
< / div >
}
2025-11-24 01:31:15 +00:00
<!-- Environment Variables -->
@if ( Object . keys ( service ( ) ! . envVars ) . length > 0 ) {
< div class = "mt-6" >
< h3 class = "text-sm font-medium text-gray-500 mb-2" > Environment Variables < / h3 >
< div class = "bg-gray-50 rounded-md p-4" >
@for ( entry of Object . entries ( service ( ) ! . envVars ) ; track entry [ 0 ] ) {
< div class = "flex justify-between py-1" >
< span class = "text-sm font-mono text-gray-700" > { { entry [ 0 ] } } < / span >
< span class = "text-sm font-mono text-gray-900" > { { entry [ 1 ] } } < / span >
< / div >
}
< / div >
< / div >
}
} @else {
<!-- Edit Form -->
< form ( ngSubmit ) = "saveService()" >
<!-- Image -->
< div class = "mb-6" >
< label for = "edit-image" class = "label" > Docker Image * < / label >
< input
type = "text"
id = "edit-image"
[ ( ngModel ) ] = "editForm.image"
name = "image"
required
placeholder = "nginx:latest"
class = "input"
/ >
< p class = "mt-1 text-sm text-gray-500" > Format : image : tag or registry / image :tag < / p >
< / div >
<!-- Port -->
< div class = "mb-6" >
< label for = "edit-port" class = "label" > Container Port * < / label >
< input
type = "number"
id = "edit-port"
[ ( ngModel ) ] = "editForm.port"
name = "port"
required
placeholder = "80"
class = "input"
/ >
< p class = "mt-1 text-sm text-gray-500" > Port that your application listens on < / p >
< / div >
<!-- Domain -->
< div class = "mb-6" >
< label for = "edit-domain" class = "label" > Domain ( Optional ) < / label >
< input
type = "text"
id = "edit-domain"
[ ( ngModel ) ] = "editForm.domain"
( ngModelChange ) = "onDomainChange()"
name = "domain"
placeholder = "app.example.com"
list = "domainList"
class = "input"
[ class . border - red - 300 ] = "domainWarning()"
/ >
< datalist id = "domainList" >
@for ( domain of availableDomains ( ) ; track domain . domain ) {
< option [ value ] = "domain.domain" > { { domain . domain } } < / option >
}
< / datalist >
@if ( domainWarning ( ) ) {
< div class = "mt-2 rounded-md bg-yellow-50 p-3" >
< div class = "flex" >
< div class = "flex-shrink-0" >
< svg class = "h-5 w-5 text-yellow-400" viewBox = "0 0 20 20" fill = "currentColor" >
< path fill-rule = "evenodd" d = "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule = "evenodd" / >
< / svg >
< / div >
< div class = "ml-3" >
< p class = "text-sm text-yellow-800" >
< strong > { { domainWarningTitle ( ) } } < / strong >
< / p >
< p class = "mt-1 text-sm text-yellow-700" > { { domainWarningMessage ( ) } } < / p >
< div class = "mt-2" >
< a routerLink = "/domains" class = "text-sm font-medium text-yellow-800 hover:text-yellow-900 underline" >
View domains & rarr ;
< / a >
< / div >
< / div >
< / div >
2025-11-18 00:03:24 +00:00
< / div >
2025-11-24 01:31:15 +00:00
} @else {
< p class = "mt-1 text-sm text-gray-500" >
Leave empty to skip automatic DNS & SSL .
@if ( availableDomains ( ) . length > 0 ) {
< span > Or select from { { availableDomains ( ) . length } } available domain ( s ) . < / span >
}
< / p >
2025-11-18 00:03:24 +00:00
}
< / div >
2025-11-24 01:31:15 +00:00
<!-- Environment Variables -->
< div class = "mb-6" >
< label class = "label" > Environment Variables < / label >
@for ( env of editEnvVars ( ) ; track $index ) {
< div class = "flex gap-2 mb-2" >
< input
type = "text"
[ ( ngModel ) ] = "env.key"
[ name ] = "'envKey' + $index"
placeholder = "KEY"
class = "input flex-1"
/ >
< input
type = "text"
[ ( ngModel ) ] = "env.value"
[ name ] = "'envValue' + $index"
placeholder = "value"
class = "input flex-1"
/ >
< button type = "button" ( click ) = "removeEnvVar($index)" class = "btn btn-danger" >
Remove
< / button >
< / div >
}
< button type = "button" ( click ) = "addEnvVar()" class = "btn btn-secondary mt-2" >
Add Environment Variable
< / button >
< / div >
@if ( error ( ) ) {
< div class = "rounded-md bg-red-50 p-4 mb-6" >
< p class = "text-sm text-red-800" > { { error ( ) } } < / p >
< / div >
}
<!-- Edit Actions -->
< div class = "flex justify-end space-x-4" >
< button type = "button" ( click ) = "cancelEditing()" class = "btn btn-secondary" [ disabled ] = "saving()" >
Cancel
< / button >
< button type = "submit" class = "btn btn-primary" [ disabled ] = "saving()" >
{ { saving ( ) ? 'Saving...' : 'Save Changes' } }
< / button >
< / div >
< / form >
2025-11-18 00:03:24 +00:00
}
< / div >
<!-- Actions -->
2025-11-24 01:31:15 +00:00
@if ( ! isEditing ( ) ) {
< div class = "card mb-6" >
< h2 class = "text-lg font-medium text-gray-900 mb-4" > Actions < / h2 >
< div class = "flex space-x-4" >
@if ( service ( ) ! . status === 'stopped' ) {
< button ( click ) = "startService()" class = "btn btn-success" > Start < / button >
}
@if ( service ( ) ! . status === 'running' ) {
< button ( click ) = "stopService()" class = "btn btn-secondary" > Stop < / button >
< button ( click ) = "restartService()" class = "btn btn-primary" > Restart < / button >
}
< button ( click ) = "deleteService()" class = "btn btn-danger" > Delete < / button >
< / div >
2025-11-18 00:03:24 +00:00
< / div >
2025-11-24 01:31:15 +00:00
}
2025-11-18 00:03:24 +00:00
<!-- Logs -->
2025-11-24 01:31:15 +00:00
@if ( ! isEditing ( ) ) {
< div class = "card" >
< div class = "flex items-center justify-between mb-4" >
< h2 class = "text-lg font-medium text-gray-900" > Logs < / h2 >
< div class = "flex items-center gap-3" >
<!-- Search -->
< input
type = "text"
[ ( ngModel ) ] = "logSearch"
( ngModelChange ) = "filterLogs()"
placeholder = "Search logs..."
class = "input text-sm w-48"
/ >
<!-- Log Level Filter -->
< select [ ( ngModel ) ] = "logLevelFilter" ( ngModelChange ) = "filterLogs()" class = "input text-sm" >
< option value = "all" > All Levels < / option >
< option value = "error" > Errors < / option >
< option value = "warn" > Warnings < / option >
< option value = "info" > Info < / option >
< option value = "debug" > Debug < / option >
< / select >
<!-- Auto - refresh toggle -->
< label class = "flex items-center text-sm text-gray-700" >
< input
type = "checkbox"
[ ( ngModel ) ] = "logsAutoRefresh"
( ngModelChange ) = "toggleLogsAutoRefresh()"
class = "mr-2"
/ >
Auto - refresh
< / label >
< button ( click ) = "refreshLogs()" class = "btn btn-secondary text-sm" [ disabled ] = "loadingLogs()" >
< svg class = "w-4 h-4" [ class.animate - spin ] = "loadingLogs()" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" > < / path >
< / svg >
< / button >
< / div >
2025-11-18 00:03:24 +00:00
< / div >
2025-11-24 01:31:15 +00:00
@if ( loadingLogs ( ) ) {
< div class = "text-center py-8" >
< div class = "inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-primary-600" > < / div >
< / div >
} @else {
< div class = "bg-gray-900 rounded-md p-4 overflow-x-auto max-h-96 overflow-y-auto" >
@if ( filteredLogs ( ) . length === 0 ) {
< p class = "text-sm text-gray-400" > No logs available < / p >
} @else {
@for ( line of filteredLogs ( ) ; track $index ) {
< div class = "text-xs font-mono mb-1" [ ngClass ] = " {
'text-red-400' : isLogLevel ( line , 'error' ) ,
'text-yellow-400' : isLogLevel ( line , 'warn' ) ,
'text-blue-300' : isLogLevel ( line , 'info' ) ,
'text-gray-400' : isLogLevel ( line , 'debug' ) ,
'text-gray-100' : ! hasLogLevel ( line )
} " > { { line } } < / div >
}
}
< / div >
}
@if ( filteredLogs ( ) . length > 0 && filteredLogs ( ) . length !== logLines ( ) . length ) {
< div class = "mt-2 text-sm text-gray-500" >
Showing { { filteredLogs ( ) . length } } of { { logLines ( ) . length } } lines
< / div >
}
< / div >
}
2025-11-18 00:03:24 +00:00
}
< / div >
` ,
} )
2025-11-24 01:31:15 +00:00
export class ServiceDetailComponent implements OnInit , OnDestroy {
2025-11-18 00:03:24 +00:00
private apiService = inject ( ApiService ) ;
private route = inject ( ActivatedRoute ) ;
private router = inject ( Router ) ;
service = signal < Service | null > ( null ) ;
logs = signal ( '' ) ;
2025-11-24 01:31:15 +00:00
logLines = signal < string [ ] > ( [ ] ) ;
filteredLogs = signal < string [ ] > ( [ ] ) ;
logSearch = '' ;
logLevelFilter = 'all' ;
logsAutoRefresh = false ;
private logsRefreshInterval? : number ;
2025-11-18 00:03:24 +00:00
loading = signal ( true ) ;
loadingLogs = signal ( false ) ;
2025-11-24 01:31:15 +00:00
// Edit mode
isEditing = signal ( false ) ;
saving = signal ( false ) ;
error = signal ( '' ) ;
editForm = {
image : '' ,
port : 80 ,
domain : '' ,
} ;
editEnvVars = signal < EnvVar [ ] > ( [ ] ) ;
// Domain validation
availableDomains = signal < Domain [ ] > ( [ ] ) ;
domainWarning = signal ( false ) ;
domainWarningTitle = signal ( '' ) ;
domainWarningMessage = signal ( '' ) ;
2025-11-18 00:03:24 +00:00
Object = Object ;
ngOnInit ( ) : void {
const name = this . route . snapshot . paramMap . get ( 'name' ) ! ;
this . loadService ( name ) ;
this . loadLogs ( name ) ;
2025-11-24 01:31:15 +00:00
this . loadDomains ( ) ;
2025-11-18 00:03:24 +00:00
}
loadService ( name : string ) : void {
this . loading . set ( true ) ;
this . apiService . getService ( name ) . subscribe ( {
next : ( response ) = > {
if ( response . success && response . data ) {
this . service . set ( response . data ) ;
}
this . loading . set ( false ) ;
} ,
error : ( ) = > {
this . loading . set ( false ) ;
this . router . navigate ( [ '/services' ] ) ;
} ,
} ) ;
}
loadLogs ( name : string ) : void {
this . loadingLogs . set ( true ) ;
this . apiService . getServiceLogs ( name ) . subscribe ( {
next : ( response ) = > {
if ( response . success && response . data ) {
this . logs . set ( response . data ) ;
2025-11-24 01:31:15 +00:00
const lines = response . data . split ( '\n' ) . filter ( ( line : string ) = > line . trim ( ) ) ;
this . logLines . set ( lines ) ;
this . filterLogs ( ) ;
2025-11-18 00:03:24 +00:00
}
this . loadingLogs . set ( false ) ;
} ,
error : ( ) = > {
this . loadingLogs . set ( false ) ;
} ,
} ) ;
}
2025-11-24 01:31:15 +00:00
filterLogs ( ) : void {
let lines = this . logLines ( ) ;
// Apply level filter
if ( this . logLevelFilter !== 'all' ) {
lines = lines . filter ( line = > this . isLogLevel ( line , this . logLevelFilter ) ) ;
}
// Apply search filter
if ( this . logSearch . trim ( ) ) {
const searchLower = this . logSearch . toLowerCase ( ) ;
lines = lines . filter ( line = > line . toLowerCase ( ) . includes ( searchLower ) ) ;
}
this . filteredLogs . set ( lines ) ;
}
isLogLevel ( line : string , level : string ) : boolean {
const lineLower = line . toLowerCase ( ) ;
if ( level === 'error' ) return lineLower . includes ( 'error' ) || lineLower . includes ( '✖' ) ;
if ( level === 'warn' ) return lineLower . includes ( 'warn' ) || lineLower . includes ( 'warning' ) ;
if ( level === 'info' ) return lineLower . includes ( 'info' ) || lineLower . includes ( 'ℹ ' ) ;
if ( level === 'debug' ) return lineLower . includes ( 'debug' ) ;
return false ;
}
hasLogLevel ( line : string ) : boolean {
return this . isLogLevel ( line , 'error' ) ||
this . isLogLevel ( line , 'warn' ) ||
this . isLogLevel ( line , 'info' ) ||
this . isLogLevel ( line , 'debug' ) ;
}
toggleLogsAutoRefresh ( ) : void {
if ( this . logsAutoRefresh ) {
this . logsRefreshInterval = window . setInterval ( ( ) = > {
this . refreshLogs ( ) ;
} , 5000 ) ; // Refresh every 5 seconds
} else {
if ( this . logsRefreshInterval ) {
clearInterval ( this . logsRefreshInterval ) ;
this . logsRefreshInterval = undefined ;
}
}
}
loadDomains ( ) : void {
this . apiService . getDomains ( ) . subscribe ( {
next : ( response ) = > {
if ( response . success && response . data ) {
const domains : Domain [ ] = response . data . map ( ( d : any ) = > ( {
domain : d.domain.domain ,
dnsProvider : d.domain.dnsProvider ,
isObsolete : d.domain.isObsolete ,
} ) ) ;
this . availableDomains . set ( domains ) ;
}
} ,
error : ( ) = > {
// Silently fail - domains list not critical
} ,
} ) ;
}
startEditing ( ) : void {
const svc = this . service ( ) ! ;
this . editForm . image = svc . image ;
this . editForm . port = svc . port ;
this . editForm . domain = svc . domain || '' ;
// Convert env vars to array
const envVars : EnvVar [ ] = [ ] ;
for ( const [ key , value ] of Object . entries ( svc . envVars || { } ) ) {
envVars . push ( { key , value } ) ;
}
this . editEnvVars . set ( envVars ) ;
this . isEditing . set ( true ) ;
this . error . set ( '' ) ;
}
cancelEditing ( ) : void {
this . isEditing . set ( false ) ;
this . error . set ( '' ) ;
this . domainWarning . set ( false ) ;
}
saveService ( ) : void {
this . error . set ( '' ) ;
this . saving . set ( true ) ;
// Convert env vars to object
const envVarsObj : Record < string , string > = { } ;
for ( const env of this . editEnvVars ( ) ) {
if ( env . key && env . value ) {
envVarsObj [ env . key ] = env . value ;
}
}
const updates = {
image : this.editForm.image ,
port : this.editForm.port ,
domain : this.editForm.domain || undefined ,
envVars : envVarsObj ,
} ;
this . apiService . updateService ( this . service ( ) ! . name , updates ) . subscribe ( {
next : ( response ) = > {
this . saving . set ( false ) ;
if ( response . success ) {
this . service . set ( response . data ! ) ;
this . isEditing . set ( false ) ;
} else {
this . error . set ( response . error || 'Failed to update service' ) ;
}
} ,
error : ( err ) = > {
this . saving . set ( false ) ;
this . error . set ( err . error ? . error || 'An error occurred' ) ;
} ,
} ) ;
}
addEnvVar ( ) : void {
this . editEnvVars . update ( ( vars ) = > [ . . . vars , { key : '' , value : '' } ] ) ;
}
removeEnvVar ( index : number ) : void {
this . editEnvVars . update ( ( vars ) = > vars . filter ( ( _ , i ) = > i !== index ) ) ;
}
onDomainChange ( ) : void {
if ( ! this . editForm . domain ) {
this . domainWarning . set ( false ) ;
return ;
}
// Extract base domain from entered domain
const parts = this . editForm . domain . split ( '.' ) ;
if ( parts . length < 2 ) {
this . domainWarning . set ( false ) ;
return ;
}
const baseDomain = parts . slice ( - 2 ) . join ( '.' ) ;
// Check if base domain exists in available domains
const matchingDomain = this . availableDomains ( ) . find (
( d ) = > d . domain === baseDomain
) ;
if ( ! matchingDomain ) {
this . domainWarning . set ( true ) ;
this . domainWarningTitle . set ( 'Domain not found' ) ;
this . domainWarningMessage . set (
` The base domain " ${ baseDomain } " is not in the Domain table. The service will update, but certificate management may not work. Sync your Cloudflare domains or manually add the domain first. `
) ;
} else if ( matchingDomain . isObsolete ) {
this . domainWarning . set ( true ) ;
this . domainWarningTitle . set ( 'Domain is obsolete' ) ;
this . domainWarningMessage . set (
` The domain " ${ baseDomain } " is marked as obsolete (likely removed from Cloudflare). Certificate management may not work properly. `
) ;
} else {
this . domainWarning . set ( false ) ;
}
}
2025-11-18 00:03:24 +00:00
refreshLogs ( ) : void {
this . loadLogs ( this . service ( ) ! . name ) ;
}
startService ( ) : void {
this . apiService . startService ( this . service ( ) ! . name ) . subscribe ( {
next : ( ) = > {
this . loadService ( this . service ( ) ! . name ) ;
} ,
} ) ;
}
stopService ( ) : void {
this . apiService . stopService ( this . service ( ) ! . name ) . subscribe ( {
next : ( ) = > {
this . loadService ( this . service ( ) ! . name ) ;
} ,
} ) ;
}
restartService ( ) : void {
this . apiService . restartService ( this . service ( ) ! . name ) . subscribe ( {
next : ( ) = > {
this . loadService ( this . service ( ) ! . name ) ;
} ,
} ) ;
}
deleteService ( ) : void {
if ( confirm ( ` Are you sure you want to delete ${ this . service ( ) ! . name } ? ` ) ) {
this . apiService . deleteService ( this . service ( ) ! . name ) . subscribe ( {
next : ( ) = > {
this . router . navigate ( [ '/services' ] ) ;
} ,
} ) ;
}
}
formatDate ( timestamp : number ) : string {
return new Date ( timestamp ) . toLocaleString ( ) ;
}
2025-11-24 01:31:15 +00:00
private toastService = inject ( ToastService ) ;
copyToken ( token : string ) : void {
navigator . clipboard . writeText ( token ) . then ( ( ) = > {
this . toastService . success ( 'Token copied to clipboard!' ) ;
} ) . catch ( ( ) = > {
this . toastService . error ( 'Failed to copy token' ) ;
} ) ;
}
registryBaseUrl = signal ( 'localhost:5000' ) ;
ngOnDestroy ( ) : void {
if ( this . logsRefreshInterval ) {
clearInterval ( this . logsRefreshInterval ) ;
}
}
2025-11-18 00:03:24 +00:00
}