fix: multiple bug fixes

isolated users, db backup, weird disconnect issues for long voice sessions,
This commit is contained in:
2026-04-24 22:19:57 +02:00
parent 44588e8789
commit bc2fa7de22
56 changed files with 1861 additions and 133 deletions

View File

@@ -0,0 +1,177 @@
import { Injector, runInInjectionContext } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import type { ServerEndpoint } from '../../domain/models/server-directory.model';
import * as serverDirectoryStorageKeys from '../../infrastructure/constants/server-directory.infrastructure.constants';
import { ServerEndpointStorageService } from '../../infrastructure/services/server-endpoint-storage.service';
import { ServerEndpointStateService } from './server-endpoint-state.service';
function createLocalStorageMock(): Storage {
const store = new Map<string, string>();
return {
get length(): number {
return store.size;
},
clear(): void {
store.clear();
},
getItem(key: string): string | null {
return store.get(key) ?? null;
},
key(index: number): string | null {
return [...store.keys()][index] ?? null;
},
removeItem(key: string): void {
store.delete(key);
},
setItem(key: string, value: string): void {
store.set(key, value);
}
};
}
Object.defineProperty(globalThis, 'localStorage', {
value: createLocalStorageMock(),
configurable: true
});
function getConfiguredDefaultServer(key: string): { key?: string; name?: string; url?: string } {
const defaultServer = environment.defaultServers.find((server) => server.key === key);
if (!defaultServer) {
throw new Error(`Missing configured default server for key: ${key}`);
}
return defaultServer;
}
function seedStoredEndpoints(endpoints: ServerEndpoint[]): void {
localStorage.setItem(serverDirectoryStorageKeys.SERVER_ENDPOINTS_STORAGE_KEY, JSON.stringify(endpoints));
}
function createService(): ServerEndpointStateService {
const injector = Injector.create({
providers: [
{
provide: ServerEndpointStorageService,
useClass: ServerEndpointStorageService,
deps: []
}
]
});
return runInInjectionContext(injector, () => new ServerEndpointStateService());
}
function getRequiredDefaultEndpoint(service: ServerEndpointStateService, defaultKey: string | undefined): ServerEndpoint {
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultKey);
if (!endpoint) {
throw new Error(`Expected default endpoint for key: ${defaultKey ?? 'unknown'}`);
}
return endpoint;
}
describe('ServerEndpointStateService', () => {
beforeEach(() => {
localStorage.clear();
});
it('reactivates configured default endpoints unless the user disabled them', () => {
const defaultServer = getConfiguredDefaultServer('toju-primary');
seedStoredEndpoints([
{
id: 'default-server',
name: 'Stored Default',
url: defaultServer.url ?? '',
isActive: false,
isDefault: true,
defaultKey: defaultServer.key,
status: 'unknown'
}
]);
const service = createService();
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultServer.key);
expect(endpoint?.isActive).toBe(true);
});
it('keeps a configured default endpoint inactive after the user turned it off', () => {
const defaultServer = getConfiguredDefaultServer('toju-primary');
seedStoredEndpoints([
{
id: 'default-server',
name: 'Stored Default',
url: defaultServer.url ?? '',
isActive: true,
isDefault: true,
defaultKey: defaultServer.key,
status: 'unknown'
}
]);
localStorage.setItem(
serverDirectoryStorageKeys.DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY,
JSON.stringify([defaultServer.key])
);
const service = createService();
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultServer.key);
expect(endpoint?.isActive).toBe(false);
});
it('keeps configured default endpoints active even when stored as incompatible unless the user disabled them', () => {
const defaultServer = getConfiguredDefaultServer('toju-primary');
seedStoredEndpoints([
{
id: 'default-server',
name: 'Stored Default',
url: defaultServer.url ?? '',
isActive: false,
isDefault: true,
defaultKey: defaultServer.key,
status: 'incompatible'
}
]);
const service = createService();
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultServer.key);
expect(endpoint?.isActive).toBe(true);
expect(endpoint?.status).toBe('incompatible');
});
it('does not deactivate configured default endpoints when compatibility checks fail', () => {
const defaultServer = getConfiguredDefaultServer('toju-primary');
const service = createService();
const endpoint = getRequiredDefaultEndpoint(service, defaultServer.key);
service.updateServerStatus(endpoint.id, 'incompatible');
expect(service.servers().find((candidate) => candidate.id === endpoint.id)?.isActive).toBe(true);
});
it('persists turning a configured default endpoint off and back on', () => {
const defaultServer = getConfiguredDefaultServer('toju-primary');
const service = createService();
const endpoint = getRequiredDefaultEndpoint(service, defaultServer.key);
service.deactivateServer(endpoint.id);
expect(service.servers().find((candidate) => candidate.id === endpoint.id)?.isActive).toBe(false);
expect(JSON.parse(
localStorage.getItem(serverDirectoryStorageKeys.DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY) ?? '[]'
)).toContain(defaultServer.key);
service.setActiveServer(endpoint.id);
expect(service.servers().find((candidate) => candidate.id === endpoint.id)?.isActive).toBe(true);
expect(localStorage.getItem(serverDirectoryStorageKeys.DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY)).toBeNull();
});
});

View File

@@ -145,6 +145,7 @@ export class ServerEndpointStateService {
if (target.isDefault) {
this.markDefaultEndpointRemoved(target);
this.clearDefaultEndpointDisabled(target);
}
const updatedEndpoints = ensureAnyActiveEndpoint(
@@ -171,6 +172,7 @@ export class ServerEndpointStateService {
this._servers.update((endpoints) => ensureAnyActiveEndpoint([...endpoints, ...restoredEndpoints]));
this.storage.clearRemovedDefaultEndpointKeys();
this.clearDisabledDefaultEndpointKeys(restoredEndpoints);
this.saveEndpoints();
return restoredEndpoints;
}
@@ -190,6 +192,12 @@ export class ServerEndpointStateService {
);
});
const target = this._servers().find((endpoint) => endpoint.id === endpointId);
if (target?.isDefault) {
this.clearDefaultEndpointDisabled(target);
}
this.saveEndpoints();
}
@@ -206,6 +214,12 @@ export class ServerEndpointStateService {
)
);
const target = this._servers().find((endpoint) => endpoint.id === endpointId);
if (target?.isDefault) {
this.markDefaultEndpointDisabled(target);
}
this.saveEndpoints();
}
@@ -225,7 +239,7 @@ export class ServerEndpointStateService {
instanceId: versions?.serverInstanceId ?? endpoint.instanceId,
status,
latency,
isActive: status === 'incompatible' ? false : endpoint.isActive,
isActive: status === 'incompatible' && !endpoint.isDefault ? false : endpoint.isActive,
serverVersion: versions?.serverVersion ?? endpoint.serverVersion,
clientVersion: versions?.clientVersion ?? endpoint.clientVersion
};
@@ -258,6 +272,7 @@ export class ServerEndpointStateService {
private reconcileStoredEndpoints(storedEndpoints: ServerEndpoint[]): ServerEndpoint[] {
const reconciled: ServerEndpoint[] = [];
const claimedDefaultKeys = new Set<string>();
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
const removedDefaultKeys = this.storage.loadRemovedDefaultEndpointKeys();
for (const endpoint of storedEndpoints) {
@@ -279,6 +294,7 @@ export class ServerEndpointStateService {
...endpoint,
name: matchedDefault.name,
url: matchedDefault.url,
isActive: this.isDefaultEndpointActive(matchedDefault.defaultKey, disabledDefaultKeys),
isDefault: true,
defaultKey: matchedDefault.defaultKey,
status: endpoint.status ?? 'unknown'
@@ -303,7 +319,7 @@ export class ServerEndpointStateService {
reconciled.push({
...defaultEndpoint,
id: uuidv4(),
isActive: defaultEndpoint.isActive
isActive: this.isDefaultEndpointActive(defaultEndpoint.defaultKey, disabledDefaultKeys)
});
}
}
@@ -324,6 +340,64 @@ export class ServerEndpointStateService {
this.storage.saveRemovedDefaultEndpointKeys(removedDefaultKeys);
}
private markDefaultEndpointDisabled(endpoint: ServerEndpoint): void {
const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);
if (!defaultKey) {
return;
}
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
disabledDefaultKeys.add(defaultKey);
this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
}
private clearDefaultEndpointDisabled(endpoint: ServerEndpoint): void {
const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);
if (!defaultKey) {
return;
}
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
if (!disabledDefaultKeys.delete(defaultKey)) {
return;
}
this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
}
private clearDisabledDefaultEndpointKeys(endpoints: ServerEndpoint[]): void {
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
let didChange = false;
for (const endpoint of endpoints) {
const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);
if (!defaultKey) {
continue;
}
didChange = disabledDefaultKeys.delete(defaultKey) || didChange;
}
if (!didChange) {
return;
}
this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
}
private isDefaultEndpointActive(
defaultKey: string,
disabledDefaultKeys: Set<string>
): boolean {
return !disabledDefaultKeys.has(defaultKey);
}
private saveEndpoints(): void {
this.storage.saveEndpoints(this._servers());
}