This commit is contained in:
Flaminel
2025-05-20 13:35:28 +03:00
parent ee02666dc1
commit 6c9b60dff5
20 changed files with 904 additions and 47 deletions

View File

@@ -41,8 +41,6 @@ public static class LoggingDI
List<string> instanceNames = [InstanceType.Sonarr.ToString(), InstanceType.Radarr.ToString(), InstanceType.Lidarr.ToString()];
int arrPadding = instanceNames.Max(x => x.Length) + 2;
// Log level is controlled by the LoggingConfigManager's level switch
// Apply padding values to templates
string consoleTemplate = consoleOutputTemplate
.Replace("CAT_PAD", catPadding.ToString())

View File

@@ -1,6 +1,8 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'logs', pathMatch: 'full' },
{ path: 'logs', loadComponent: () => import('./logging/logs-viewer/logs-viewer.component').then(m => m.LogsViewerComponent) }
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', loadComponent: () => import('./dashboard/dashboard-page/dashboard-page.component').then(m => m.DashboardPageComponent) },
{ path: 'logs', loadComponent: () => import('./logging/logs-viewer/logs-viewer.component').then(m => m.LogsViewerComponent) },
{ path: 'settings', loadComponent: () => import('./settings/settings-page/settings-page.component').then(m => m.SettingsPageComponent) }
];

View File

@@ -0,0 +1,32 @@
/**
* Models for SignalR connections and messages
*/
/**
* Configuration options for a SignalR hub connection
*/
export interface SignalRHubConfig {
/** URL to the SignalR hub endpoint */
hubUrl: string;
/** Maximum number of reconnection attempts (0 for infinite) */
maxReconnectAttempts: number;
/** Initial delay between reconnection attempts in milliseconds (will be subject to backoff) */
reconnectDelayMs: number;
/** Maximum size of the message buffer */
bufferSize: number;
/** Interval in milliseconds to check connection health (0 to disable) */
healthCheckIntervalMs: number;
}
/**
* Standard log entry message format received from the server
*/
export interface LogEntry {
timestamp: Date;
level: string;
message: string;
exception?: string;
category?: string;
jobName?: string;
instanceName?: string;
}

View File

@@ -0,0 +1,269 @@
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import * as signalR from '@microsoft/signalr';
import { SignalRHubConfig } from '../models/signalr.models';
/**
* Base service for SignalR hub connections.
* Provides common functionality for connecting to, monitoring, and managing SignalR hubs.
*
* @typeParam T - The type of messages that will be received from this hub
*/
@Injectable()
export abstract class BaseSignalRService<T> implements OnDestroy {
protected hubConnection!: signalR.HubConnection;
protected connectionStatusSubject = new BehaviorSubject<boolean>(false);
protected messageSubject = new BehaviorSubject<T[]>([]);
protected destroy$ = new Subject<void>();
protected reconnectAttempts = 0;
protected connectionHealthCheckInterval: any;
protected messageBuffer: T[] = [];
/**
* Initialize a base SignalR hub connection service
*
* @param config - Configuration for the hub connection
* @param messageEventName - The name of the event that delivers messages from the server
*/
constructor(
protected config: SignalRHubConfig,
protected messageEventName: string
) {}
/**
* Start the SignalR connection to the hub
* @returns Promise that resolves when the connection is established
*/
public startConnection(): Promise<void> {
if (this.hubConnection &&
this.hubConnection.state !== signalR.HubConnectionState.Disconnected) {
return Promise.resolve();
}
// Build a new connection if needed
if (!this.hubConnection) {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(this.config.hubUrl)
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
if (this.config.maxReconnectAttempts > 0 &&
retryContext.previousRetryCount >= this.config.maxReconnectAttempts) {
return null; // Stop trying after max attempts
}
// Implement exponential backoff with a maximum delay of 30 seconds
return Math.min(
this.config.reconnectDelayMs * Math.pow(2, retryContext.previousRetryCount),
30000
);
}
})
.build();
this.registerSignalREvents();
}
// Start health check interval if configured
this.startHealthCheck();
return this.hubConnection.start()
.then(() => {
console.log(`SignalR connection started to ${this.config.hubUrl}`);
this.connectionStatusSubject.next(true);
this.reconnectAttempts = 0;
this.onConnectionEstablished();
})
.catch(err => {
console.error(`Error connecting to ${this.config.hubUrl}:`, err);
this.connectionStatusSubject.next(false);
// Always try to reconnect, optionally limited by maxReconnectAttempts
const infiniteReconnect = this.config.maxReconnectAttempts === 0;
if (infiniteReconnect || this.reconnectAttempts < this.config.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(
this.config.reconnectDelayMs * Math.pow(1.5, this.reconnectAttempts),
30000
);
console.log(`Attempting to reconnect (${this.reconnectAttempts}) in ${delay}ms...`);
setTimeout(() => this.startConnection(), delay);
}
throw err;
});
}
/**
* Stop the SignalR connection
* @returns Promise that resolves when the connection is stopped
*/
public stopConnection(): Promise<void> {
this.stopHealthCheck();
if (!this.hubConnection) {
return Promise.resolve();
}
return this.hubConnection.stop()
.then(() => {
console.log(`SignalR connection to ${this.config.hubUrl} stopped`);
this.connectionStatusSubject.next(false);
})
.catch(err => {
console.error(`Error stopping connection to ${this.config.hubUrl}:`, err);
throw err;
});
}
/**
* Register event handlers for SignalR events
*/
protected registerSignalREvents(): void {
// Handle incoming messages
this.hubConnection.on(this.messageEventName, (message: T) => {
this.processMessage(message);
});
// Handle reconnection events
this.hubConnection.onreconnected(() => {
console.log(`SignalR connection reconnected to ${this.config.hubUrl}`);
this.connectionStatusSubject.next(true);
this.reconnectAttempts = 0;
this.onConnectionEstablished();
});
this.hubConnection.onreconnecting(() => {
console.log(`SignalR connection reconnecting to ${this.config.hubUrl}...`);
this.connectionStatusSubject.next(false);
});
this.hubConnection.onclose(() => {
console.log(`SignalR connection to ${this.config.hubUrl} closed`);
this.connectionStatusSubject.next(false);
// Try to reconnect if the connection was closed unexpectedly
if (this.shouldAttemptReconnect()) {
this.startConnection();
}
});
}
/**
* Start the health check timer to periodically verify connection status
*/
protected startHealthCheck(): void {
this.stopHealthCheck();
if (this.config.healthCheckIntervalMs > 0) {
this.connectionHealthCheckInterval = setInterval(() => {
this.checkConnectionHealth();
}, this.config.healthCheckIntervalMs);
}
}
/**
* Stop the health check timer
*/
protected stopHealthCheck(): void {
if (this.connectionHealthCheckInterval) {
clearInterval(this.connectionHealthCheckInterval);
this.connectionHealthCheckInterval = null;
}
}
/**
* Check the health of the connection and attempt to reconnect if needed
*/
protected checkConnectionHealth(): void {
if (!this.hubConnection ||
this.hubConnection.state === signalR.HubConnectionState.Disconnected) {
console.log('Health check detected disconnected state, attempting to reconnect...');
this.startConnection();
}
}
/**
* Process a message received from the hub
* @param message - The message received from the hub
*/
protected processMessage(message: T): void {
// Add to buffer
this.addToBuffer(message);
// Update the subject with current messages
const currentMessages = this.messageSubject.value;
this.messageSubject.next([...currentMessages, message]);
}
/**
* Add a message to the buffer
* @param message - The message to add to the buffer
*/
protected addToBuffer(message: T): void {
this.messageBuffer.push(message);
// Trim buffer if it exceeds the limit
if (this.messageBuffer.length > this.config.bufferSize) {
this.messageBuffer.shift();
}
}
/**
* Called when a connection is established or re-established
* Override in derived classes to perform hub-specific initialization
*/
protected onConnectionEstablished(): void {
// Override in derived classes
}
/**
* Determine if a reconnection attempt should be made
* @returns True if a reconnection attempt should be made
*/
protected shouldAttemptReconnect(): boolean {
// By default, always try to reconnect unless max attempts reached
return this.config.maxReconnectAttempts === 0 ||
this.reconnectAttempts < this.config.maxReconnectAttempts;
}
/**
* Force a reconnection attempt, even if max attempts have been reached
*/
public forceReconnect(): void {
this.reconnectAttempts = 0;
this.startConnection();
}
/**
* Get the buffered messages
* @returns A copy of the message buffer
*/
public getBufferedMessages(): T[] {
return [...this.messageBuffer];
}
/**
* Get messages as an observable
* @returns An observable that emits the current messages and all future messages
*/
public getMessages(): Observable<T[]> {
return this.messageSubject.asObservable();
}
/**
* Get connection status as an observable
* @returns An observable that emits the current connection status and all future status changes
*/
public getConnectionStatus(): Observable<boolean> {
return this.connectionStatusSubject.asObservable();
}
/**
* Clean up resources
*/
ngOnDestroy(): void {
this.stopHealthCheck();
this.stopConnection();
}
}

View File

@@ -0,0 +1,61 @@
import { Injectable, inject } from '@angular/core';
import { BaseSignalRService } from './base-signalr.service';
import { LogEntry, SignalRHubConfig } from '../models/signalr.models';
// Note: We'll define a fallback API URL if environment is not available yet
const API_URL = 'http://localhost:5000';
/**
* Service for connecting to the logs SignalR hub
*/
@Injectable({
providedIn: 'root'
})
export class LogHubService extends BaseSignalRService<LogEntry> {
constructor() {
// Default configuration for the logs hub
const config: SignalRHubConfig = {
hubUrl: `${API_URL}/api/hubs/logs`,
maxReconnectAttempts: 0, // Infinite reconnection attempts
reconnectDelayMs: 2000,
bufferSize: 100,
healthCheckIntervalMs: 30000 // Check connection every 30 seconds
};
super(config, 'ReceiveLog');
}
/**
* Request recent logs from the server
*/
public requestRecentLogs(): void {
if (this.hubConnection &&
this.hubConnection.state === signalR.HubConnectionState.Connected) {
this.hubConnection.invoke('RequestRecentLogs')
.catch(err => console.error('Error while requesting recent logs:', err));
}
}
/**
* Override to request recent logs when connection is established
*/
protected override onConnectionEstablished(): void {
this.requestRecentLogs();
}
/**
* Get the buffered logs
*/
public getBufferedLogs(): LogEntry[] {
return this.getBufferedMessages();
}
/**
* Get logs as an observable
*/
public getLogs() {
return this.getMessages();
}
}
// Fix missing signalR import
import * as signalR from '@microsoft/signalr';

View File

@@ -0,0 +1,66 @@
import { Injectable } from '@angular/core';
import { BaseSignalRService } from './base-signalr.service';
import { SignalRHubConfig } from '../models/signalr.models';
import { environment } from '../../../environments/environment';
import * as signalR from '@microsoft/signalr';
/**
* System status message from the system hub
*/
export interface SystemStatus {
timestamp: Date;
status: 'online' | 'offline' | 'degraded';
message: string;
serverVersion?: string;
components?: {
name: string;
status: 'online' | 'offline' | 'degraded';
}[];
}
/**
* Service for connecting to the system monitoring SignalR hub
*/
@Injectable({
providedIn: 'root'
})
export class SystemHubService extends BaseSignalRService<SystemStatus> {
constructor() {
// Default configuration for the system hub
const config: SignalRHubConfig = {
hubUrl: environment.apiUrl ? `${environment.apiUrl}/api/hubs/system` : 'http://localhost:5000/api/hubs/system',
maxReconnectAttempts: 0, // Infinite reconnection attempts
reconnectDelayMs: 2000,
bufferSize: 10, // Only need a few status updates
healthCheckIntervalMs: 30000 // Check connection every 30 seconds
};
super(config, 'ReceiveSystemStatus');
}
/**
* Request the current system status
*/
public requestSystemStatus(): void {
if (this.hubConnection &&
this.hubConnection.state === signalR.HubConnectionState.Connected) {
this.hubConnection.invoke('RequestSystemStatus')
.catch(err => console.error('Error while requesting system status:', err));
}
}
/**
* Override to request system status when connection is established
*/
protected override onConnectionEstablished(): void {
this.requestSystemStatus();
}
/**
* Get the latest system status
*/
public getLatestStatus(): SystemStatus | undefined {
const messages = this.getBufferedMessages();
return messages.length > 0 ? messages[messages.length - 1] : undefined;
}
}

View File

@@ -0,0 +1,38 @@
<div class="dashboard-container">
<h1>Dashboard</h1>
<div class="grid">
<div class="col-12 md:col-6 lg:col-4">
<p-card header="Logs Summary" subheader="Recent logging activity">
<div class="card-content">
<p>Monitor your application logs and error statuses.</p>
<div class="card-footer">
<button pButton label="View Logs" icon="pi pi-list" routerLink="/logs"></button>
</div>
</div>
</p-card>
</div>
<div class="col-12 md:col-6 lg:col-4">
<p-card header="System Status" subheader="Current health status">
<div class="card-content">
<p>All systems operational</p>
<div class="card-footer">
<button pButton label="Details" icon="pi pi-server" class="p-button-outlined"></button>
</div>
</div>
</p-card>
</div>
<div class="col-12 md:col-6 lg:col-4">
<p-card header="Available Updates" subheader="System updates">
<div class="card-content">
<p>Your application is up to date.</p>
<div class="card-footer">
<button pButton label="Check for Updates" icon="pi pi-refresh" class="p-button-secondary"></button>
</div>
</div>
</p-card>
</div>
</div>
</div>

View File

@@ -0,0 +1,37 @@
.dashboard-container {
padding: 1.5rem;
h1 {
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 600;
}
.card-content {
min-height: 100px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-footer {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
}
::ng-deep .p-card {
height: 100%;
.p-card-body {
height: 100%;
display: flex;
flex-direction: column;
}
.p-card-content {
flex-grow: 1;
padding-bottom: 0;
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardPageComponent } from './dashboard-page.component';
describe('DashboardPageComponent', () => {
let component: DashboardPageComponent;
let fixture: ComponentFixture<DashboardPageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DashboardPageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DashboardPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
// PrimeNG Components
import { CardModule } from 'primeng/card';
import { ButtonModule } from 'primeng/button';
@Component({
selector: 'app-dashboard-page',
standalone: true,
imports: [
CommonModule,
CardModule,
ButtonModule
],
templateUrl: './dashboard-page.component.html',
styleUrl: './dashboard-page.component.scss'
})
export class DashboardPageComponent {
}

View File

@@ -2,6 +2,12 @@
<!-- Top Bar -->
<p-toolbar styleClass="top-bar">
<div class="p-toolbar-group-start">
<button pButton
class="p-button-text p-button-rounded sidebar-toggle-mobile"
icon="pi pi-bars"
(click)="toggleMobileSidebar()"
aria-label="Toggle menu">
</button>
<div class="logo-container">
<span class="app-name">CleanupErr</span>
</div>
@@ -15,10 +21,51 @@
</div>
</p-toolbar>
<!-- Main Content -->
<div class="layout-content">
<div class="layout-content-inner">
<router-outlet></router-outlet>
<!-- Sidebar for Mobile -->
<p-sidebar [visible]="mobileSidebarVisible()" (visibleChange)="mobileSidebarVisible.set($event)" position="left" styleClass="mobile-sidebar">
<div class="sidebar-content">
<div class="sidebar-header">
<h3>CleanupErr</h3>
</div>
<div class="sidebar-menu">
<ul class="menu-list">
<li *ngFor="let item of menuItems" class="menu-item">
<a [routerLink]="item.route" routerLinkActive="active-menu-link" [routerLinkActiveOptions]="{exact: item.route === '/dashboard'}"
(click)="toggleMobileSidebar()">
<i class="{{item.icon}}"></i>
<span>{{item.label}}</span>
<span class="badge" *ngIf="item.badge">{{item.badge}}</span>
</a>
</li>
</ul>
</div>
</div>
</p-sidebar>
<div class="layout-main">
<!-- Sidebar for Desktop -->
<div class="sidebar">
<div class="sidebar-content">
<div class="sidebar-logo">
<span class="app-logo">CE</span>
</div>
<ul class="menu-list">
<li *ngFor="let item of menuItems" class="menu-item">
<a [routerLink]="item.route" routerLinkActive="active-menu-link" [routerLinkActiveOptions]="{exact: item.route === '/dashboard'}">
<i class="{{item.icon}}"></i>
<span>{{item.label}}</span>
<span class="badge" *ngIf="item.badge">{{item.badge}}</span>
</a>
</li>
</ul>
</div>
</div>
<!-- Main Content -->
<div class="layout-content">
<div class="layout-content-inner">
<router-outlet></router-outlet>
</div>
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@
flex-direction: column;
background-color: var(--surface-ground);
color: var(--text-color);
transition: background-color 0.2s, color 0.2s;
transition: all 0.3s ease;
&.dark-mode {
// Dark mode variables are applied via documentElement.classList.add('dark')
@@ -13,42 +13,133 @@
}
.top-bar {
background-color: var(--surface-card);
background-color: var(--surface-card) !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
padding: 0.5rem 1.5rem;
border-bottom: 1px solid var(--surface-border);
position: sticky;
top: 0;
z-index: 999;
z-index: 1000;
}
.logo-container {
.logo-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.app-name {
font-size: 1.25rem;
font-weight: 600;
letter-spacing: 0.5px;
}
.theme-switch {
display: flex;
align-items: center;
gap: 0.5rem;
.pi-sun {
color: var(--yellow-500);
}
.pi-moon {
color: var(--indigo-300);
}
}
.layout-main {
display: flex;
flex: 1;
position: relative;
}
.sidebar-toggle-mobile {
display: none;
margin-right: 0.5rem;
}
// Desktop Sidebar
.sidebar {
width: 240px;
background-color: var(--surface-overlay);
border-right: 1px solid var(--surface-border);
height: calc(100vh - 4rem); // Adjust based on your toolbar height
position: sticky;
top: 4rem; // Toolbar height
z-index: 998;
transition: width 0.3s;
.sidebar-content {
display: flex;
align-items: center;
flex-direction: column;
height: 100%;
.app-name {
font-size: 1.5rem;
font-weight: 600;
color: var(--primary-color);
margin-left: 0.5rem;
.sidebar-logo {
display: flex;
justify-content: center;
padding: 1.5rem 0;
border-bottom: 1px solid var(--surface-border);
.app-logo {
width: 40px;
height: 40px;
background-color: var(--primary-color);
color: var(--primary-color-text);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 18px;
}
}
}
}
.theme-switch {
display: flex;
align-items: center;
gap: 0.5rem;
.menu-list {
list-style: none;
padding: 0.5rem;
margin: 0;
.menu-item {
margin-bottom: 0.25rem;
i {
font-size: 1.25rem;
color: var(--text-color-secondary);
}
.pi-sun {
color: var(--yellow-500);
}
.pi-moon {
color: var(--blue-500);
a {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-radius: 8px;
color: var(--text-color);
text-decoration: none;
transition: all 0.2s;
gap: 0.75rem;
&:hover {
background-color: var(--surface-hover);
}
&.active-menu-link {
background-color: var(--primary-100);
color: var(--primary-color);
font-weight: 500;
i {
color: var(--primary-color);
}
}
i {
font-size: 1.2rem;
color: var(--text-color-secondary);
}
.badge {
margin-left: auto;
background-color: var(--primary-color);
color: var(--primary-color-text);
padding: 0.25rem 0.5rem;
border-radius: 8px;
font-size: 0.75rem;
}
}
}
}
@@ -57,8 +148,10 @@
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: calc(100vh - 4rem);
padding: 1.5rem;
.layout-content-inner {
flex: 1;
border-radius: var(--border-radius);

View File

@@ -1,5 +1,5 @@
import { Component, inject, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
import { CommonModule } from '@angular/common';
// PrimeNG Imports
@@ -7,6 +7,15 @@ import { ButtonModule } from 'primeng/button';
import { ToolbarModule } from 'primeng/toolbar';
import { InputSwitchModule } from 'primeng/inputswitch';
import { FormsModule } from '@angular/forms';
import { MenuModule } from 'primeng/menu';
import { SidebarModule } from 'primeng/sidebar';
interface MenuItem {
label: string;
icon: string;
route: string;
badge?: string;
}
@Component({
selector: 'app-main-layout',
@@ -14,16 +23,28 @@ import { FormsModule } from '@angular/forms';
imports: [
CommonModule,
RouterOutlet,
RouterLink,
RouterLinkActive,
ButtonModule,
ToolbarModule,
InputSwitchModule,
FormsModule
FormsModule,
MenuModule,
SidebarModule
],
templateUrl: './main-layout.component.html',
styleUrl: './main-layout.component.scss'
})
export class MainLayoutComponent {
darkMode = signal<boolean>(false);
menuItems: MenuItem[] = [
{ label: 'Dashboard', icon: 'pi pi-home', route: '/dashboard' },
{ label: 'Logs', icon: 'pi pi-list', route: '/logs' },
{ label: 'Settings', icon: 'pi pi-cog', route: '/settings' }
];
// Mobile menu state
mobileSidebarVisible = signal<boolean>(false);
constructor() {
// Initialize theme based on system preference
@@ -45,4 +66,8 @@ export class MainLayoutComponent {
documentElement.style.colorScheme = 'light';
}
}
toggleMobileSidebar(): void {
this.mobileSidebarVisible.update(value => !value);
}
}

View File

@@ -14,8 +14,9 @@ import { ToolbarModule } from 'primeng/toolbar';
import { TooltipModule } from 'primeng/tooltip';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
// Services
import { LogEntry, SignalrService } from '../../core/services/signalr.service';
// Services & Models
import { LogHubService } from '../../core/services/log-hub.service';
import { LogEntry } from '../../core/models/signalr.models';
@Component({
selector: 'app-logs-viewer',
@@ -34,12 +35,12 @@ import { LogEntry, SignalrService } from '../../core/services/signalr.service';
TooltipModule,
ProgressSpinnerModule
],
providers: [SignalrService],
providers: [LogHubService],
templateUrl: './logs-viewer.component.html',
styleUrl: './logs-viewer.component.scss'
})
export class LogsViewerComponent implements OnInit, OnDestroy {
private signalrService = inject(SignalrService);
private logHubService = inject(LogHubService);
private destroy$ = new Subject<void>();
// Signals for reactive state
@@ -87,18 +88,18 @@ export class LogsViewerComponent implements OnInit, OnDestroy {
ngOnInit(): void {
// Connect to SignalR hub
this.signalrService.startConnection()
.catch((error: Error) => console.error('Failed to connect to SignalR hub:', error));
this.logHubService.startConnection()
.catch((error: Error) => console.error('Failed to connect to log hub:', error));
// Subscribe to logs
this.signalrService.getLogs()
this.logHubService.getLogs()
.pipe(takeUntil(this.destroy$))
.subscribe((logs: LogEntry[]) => {
this.logs.set(logs);
});
// Subscribe to connection status
this.signalrService.getConnectionStatus()
this.logHubService.getConnectionStatus()
.pipe(takeUntil(this.destroy$))
.subscribe((status: boolean) => {
this.isConnected.set(status);
@@ -150,7 +151,7 @@ export class LogsViewerComponent implements OnInit, OnDestroy {
}
refresh(): void {
this.signalrService.requestRecentLogs();
this.logHubService.requestRecentLogs();
}
hasJobInfo(): boolean {

View File

@@ -0,0 +1,41 @@
<div class="settings-container">
<h1>Settings</h1>
<p-accordion [multiple]="true" [activeIndex]="[0]">
<p-accordionTab header="API Connection Settings">
<div class="field-row">
<label for="apiUrl">API URL</label>
<input id="apiUrl" type="text" pInputText [(ngModel)]="apiUrl" />
</div>
</p-accordionTab>
<p-accordionTab header="Logging Settings">
<div class="field-row">
<label for="enableLogs">Enable Logging</label>
<p-inputSwitch id="enableLogs" [(ngModel)]="enableLogs"></p-inputSwitch>
</div>
<div class="field-row">
<label for="enableNotifications">Show Log Notifications</label>
<p-inputSwitch id="enableNotifications" [(ngModel)]="enableNotifications"></p-inputSwitch>
</div>
</p-accordionTab>
<p-accordionTab header="UI Settings">
<div class="field-row">
<label for="autoRefresh">Auto-refresh Logs</label>
<p-inputSwitch id="autoRefresh" [(ngModel)]="autoRefresh"></p-inputSwitch>
</div>
<div class="field-row" *ngIf="autoRefresh">
<label for="refreshInterval">Refresh Interval (seconds)</label>
<input id="refreshInterval" type="number" pInputText [(ngModel)]="refreshInterval" min="5" max="300" />
</div>
</p-accordionTab>
</p-accordion>
<div class="button-container">
<button pButton label="Save Changes" icon="pi pi-save" class="p-button-success"></button>
<button pButton label="Reset to Defaults" icon="pi pi-refresh" class="p-button-secondary p-button-outlined"></button>
</div>
</div>

View File

@@ -0,0 +1,32 @@
.settings-container {
padding: 1.5rem;
h1 {
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 600;
}
.field-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
label {
font-weight: 500;
margin-right: 1rem;
}
input {
max-width: 300px;
}
}
.button-container {
margin-top: 1.5rem;
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsPageComponent } from './settings-page.component';
describe('SettingsPageComponent', () => {
let component: SettingsPageComponent;
let fixture: ComponentFixture<SettingsPageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SettingsPageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SettingsPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,34 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
// PrimeNG Components
import { CardModule } from 'primeng/card';
import { InputTextModule } from 'primeng/inputtext';
import { InputSwitchModule } from 'primeng/inputswitch';
import { ButtonModule } from 'primeng/button';
import { AccordionModule } from 'primeng/accordion';
@Component({
selector: 'app-settings-page',
standalone: true,
imports: [
CommonModule,
FormsModule,
CardModule,
InputTextModule,
InputSwitchModule,
ButtonModule,
AccordionModule
],
templateUrl: './settings-page.component.html',
styleUrl: './settings-page.component.scss'
})
export class SettingsPageComponent {
// Sample settings
apiUrl = 'http://localhost:5000';
enableLogs = true;
enableNotifications = true;
autoRefresh = false;
refreshInterval = 30;
}

View File

@@ -0,0 +1,7 @@
/**
* Environment configuration for production
*/
export const environment = {
production: true,
apiUrl: '' // Set this dynamically or through build configuration
};

View File

@@ -0,0 +1,7 @@
/**
* Environment configuration for development
*/
export const environment = {
production: false,
apiUrl: 'http://localhost:5000'
};