diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 3f873892..87ebd18e 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -29,6 +29,8 @@ import { ToastService } from './core/services/toast.service';
import { AppNetworkInitService, IBootstrapIssue } from './core/services/app-initNetwork.service';
import { SsoRedirectService } from './core/services/sso-redirect.service';
import { DashboardHistorySeriesSyncService } from './core/services/dashboard-history-series-sync.service';
+import { Title } from '@angular/platform-browser';
+import { resolveBrowserTabTitle } from './core/utils/browser-tab-title.util';
@Component({
selector: 'app-root',
@@ -64,6 +66,8 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly _uiEvent = inject(uiEventService);
private readonly _dialog = inject(DialogService);
public readonly settings = inject(SettingsService);
+ private readonly _titleService = inject(Title);
+ private readonly _browserTabTitle = toSignal(this.settings.getBrowserTabTitleAsO(), { initialValue: 'SKip' });
private readonly _responsive = inject(BreakpointObserver);
private readonly _destroyRef = inject(DestroyRef);
private readonly _notificationOverlay = inject(NotificationOverlayService);
@@ -92,6 +96,11 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly _hotkeyHandler = (key: string) => this.handleKeyDown(key);
constructor() {
+ // Keep the browser tab title (document.title) in sync with the user setting (#1055).
+ effect(() => {
+ this._titleService.setTitle(resolveBrowserTabTitle(this._browserTabTitle()));
+ });
+
effect(() => {
if (this.settings.configUpgrade()) {
const liveVersion = this.settings.getConfigVersion();
diff --git a/src/app/core/components/options/display/display.component.html b/src/app/core/components/options/display/display.component.html
index 46a2927c..f0da44ea 100644
--- a/src/app/core/components/options/display/display.component.html
+++ b/src/app/core/components/options/display/display.component.html
@@ -55,6 +55,26 @@
Enable light theme for brighter display settings.
+
+
+
+ Browser Tab
+
+ @if (!isPhonePortrait().matches) {
+
+ Set the browser tab title to tell multiple SKip instances apart.
+
+ }
+
+
+ Browser tab title
+
+
+
diff --git a/src/app/core/components/options/display/display.component.ts b/src/app/core/components/options/display/display.component.ts
index c7a14f67..4fcd016a 100644
--- a/src/app/core/components/options/display/display.component.ts
+++ b/src/app/core/components/options/display/display.component.ts
@@ -57,6 +57,7 @@ export class SettingsDisplayComponent implements OnInit {
protected isLightTheme = model(false);
protected isRemoteControl = model(false);
protected instanceName = model('');
+ protected browserTabTitle = model('SKip');
protected splitShellEnabled = model(false);
protected splitShellSide = model<'left' | 'right'>('left');
protected splitShellSwipeDisabled = model(false);
@@ -78,6 +79,7 @@ export class SettingsDisplayComponent implements OnInit {
this.isRedNightMode.set(this.settings.getRedNightMode());
this.isRemoteControl.set(this.settings.getIsRemoteControl());
this.instanceName.set(this.settings.getInstanceName());
+ this.browserTabTitle.set(this.settings.getBrowserTabTitle());
this.splitShellEnabled.set(this.settings.getSplitShellEnabled());
this.splitShellSide.set(this.settings.getSplitShellSide());
this.splitShellSwipeDisabled.set(this.settings.getSplitShellSwipeDisabled());
@@ -127,6 +129,7 @@ export class SettingsDisplayComponent implements OnInit {
this.settings.setSplitShellSide(this.splitShellSide());
this.settings.setSplitShellSwipeDisabled(this.splitShellSwipeDisabled());
this.settings.setWidgetHistoryDisabled(this.widgetHistoryDisabled());
+ this.settings.setBrowserTabTitle(this.browserTabTitle());
this.settings.setDisablePathValidation(this.isPathValidationDisabled());
// Await the server write; setKipPluginConfig() returns a Promise, so the old
// `if (!this.setKipPluginConfig())` was always false (a Promise is truthy) —
diff --git a/src/app/core/interfaces/app-settings.interfaces.ts b/src/app/core/interfaces/app-settings.interfaces.ts
index 27aff150..903986f9 100644
--- a/src/app/core/interfaces/app-settings.interfaces.ts
+++ b/src/app/core/interfaces/app-settings.interfaces.ts
@@ -49,6 +49,7 @@ export interface IAppConfig {
splitShellSwipeDisabled?: boolean;
splitShellWidth?: number;
widgetHistoryDisabled?: boolean;
+ browserTabTitle?: string;
}
export interface IThemeConfig {
diff --git a/src/app/core/services/settings.service.ts b/src/app/core/services/settings.service.ts
index 01e0f0fb..3fec2596 100644
--- a/src/app/core/services/settings.service.ts
+++ b/src/app/core/services/settings.service.ts
@@ -40,6 +40,7 @@ export class SettingsService {
private nightModeBrightness: BehaviorSubject = new BehaviorSubject(1);
private isRemoteControl: BehaviorSubject = new BehaviorSubject(false);
private instanceName: BehaviorSubject = new BehaviorSubject('');
+ private browserTabTitle: BehaviorSubject = new BehaviorSubject('SKip');
private splitShellEnabled: BehaviorSubject = new BehaviorSubject(false);
private splitShellSide: BehaviorSubject<'left' | 'right'> = new BehaviorSubject<'left' | 'right'>('left');
private splitShellSwipeDisabled: BehaviorSubject = new BehaviorSubject(false);
@@ -248,6 +249,12 @@ export class SettingsService {
this._dashboards = this.activeConfig.dashboards;
}
+ if (this.activeConfig.app.browserTabTitle === undefined) {
+ this.browserTabTitle.next('SKip');
+ } else {
+ this.browserTabTitle.next(this.activeConfig.app.browserTabTitle);
+ }
+
if (this.activeConfig.app.splitShellEnabled === undefined) {
this.setSplitShellEnabled(false);
} else {
@@ -457,6 +464,26 @@ export class SettingsService {
this.saveConnectionConfigToLocalStorage();
}
+ // Browser tab title (document.title)
+ public getBrowserTabTitleAsO() {
+ return this.browserTabTitle.asObservable();
+ }
+
+ public getBrowserTabTitle(): string {
+ return this.browserTabTitle.getValue();
+ }
+
+ public setBrowserTabTitle(title: string) {
+ this.browserTabTitle.next(title);
+ const appConf = this.buildAppStorageObject();
+
+ if (this.useSharedConfig) {
+ this.storage.patchConfig('IAppConfig', appConf);
+ } else {
+ this.saveAppConfigToLocalStorage();
+ }
+ }
+
public getDisablePathValidation(): boolean {
return this.disablePathValidation;
}
@@ -726,7 +753,8 @@ export class SettingsService {
splitShellSide: this.splitShellSide.getValue() ?? 'right',
splitShellWidth: this.splitShellWidth.getValue() ?? 0.5,
splitShellSwipeDisabled: this.splitShellSwipeDisabled.getValue(),
- widgetHistoryDisabled: this.widgetHistoryDisabled.getValue()
+ widgetHistoryDisabled: this.widgetHistoryDisabled.getValue(),
+ browserTabTitle: this.browserTabTitle.getValue()
}
return storageObject;
}
diff --git a/src/app/core/utils/browser-tab-title.util.spec.ts b/src/app/core/utils/browser-tab-title.util.spec.ts
new file mode 100644
index 00000000..94dce47a
--- /dev/null
+++ b/src/app/core/utils/browser-tab-title.util.spec.ts
@@ -0,0 +1,16 @@
+import { describe, it, expect } from 'vitest';
+import { resolveBrowserTabTitle } from './browser-tab-title.util';
+
+describe('resolveBrowserTabTitle (#1055)', () => {
+ it('defaults to SKip when the value is missing or blank', () => {
+ expect(resolveBrowserTabTitle(undefined)).toBe('SKip');
+ expect(resolveBrowserTabTitle(null)).toBe('SKip');
+ expect(resolveBrowserTabTitle('')).toBe('SKip');
+ expect(resolveBrowserTabTitle(' ')).toBe('SKip');
+ });
+
+ it('uses the trimmed configured value when set', () => {
+ expect(resolveBrowserTabTitle('Mast-SKip')).toBe('Mast-SKip');
+ expect(resolveBrowserTabTitle(' Port Engine ')).toBe('Port Engine');
+ });
+});
diff --git a/src/app/core/utils/browser-tab-title.util.ts b/src/app/core/utils/browser-tab-title.util.ts
new file mode 100644
index 00000000..93fd7422
--- /dev/null
+++ b/src/app/core/utils/browser-tab-title.util.ts
@@ -0,0 +1,8 @@
+/**
+ * Resolves the browser tab title (document.title) from the user-configured value.
+ * Falls back to 'SKip' when the value is empty/whitespace so the tab is never blank (#1055).
+ */
+export function resolveBrowserTabTitle(value?: string | null): string {
+ const trimmed = (value ?? '').trim();
+ return trimmed.length ? trimmed : 'SKip';
+}
diff --git a/src/default-config/config.blank.const.ts b/src/default-config/config.blank.const.ts
index 7e5db14d..d2e2cbb7 100644
--- a/src/default-config/config.blank.const.ts
+++ b/src/default-config/config.blank.const.ts
@@ -14,7 +14,8 @@ export const DefaultAppConfig: IAppConfig = {
"splitShellEnabled": true,
"splitShellSide": "left",
"splitShellSwipeDisabled": false,
- "splitShellWidth": 0.5
+ "splitShellWidth": 0.5,
+ "browserTabTitle": "SKip"
}
export const DefaultThemeConfig: IThemeConfig = {