Enhance SwarmUI integration by adding swarm connection status to the API and updating client-side components to reflect this status. Improved localization strings for various SwarmUI states and refactored ApplicantsDialog and PersonCard components to utilize the new connection information, ensuring a more responsive user experience.
This commit is contained in:
@@ -146,6 +146,11 @@ const ru = {
|
||||
peoplePortraitGenerating: 'Генерация…',
|
||||
peoplePortraitMissing: 'Ещё не сгенерировано.',
|
||||
peoplePortraitUnavailable: 'SwarmUI не настроен на сервере.',
|
||||
peoplePortraitSwarmDisabled: 'SwarmUI: не настроен',
|
||||
peoplePortraitSwarmChecking: 'SwarmUI: проверка…',
|
||||
peoplePortraitSwarmConnected: 'SwarmUI: подключён',
|
||||
peoplePortraitSwarmDisconnected: 'SwarmUI: нет связи',
|
||||
peoplePortraitSwarmOffline: 'SwarmUI недоступен — проверьте, что сервис запущен.',
|
||||
peoplePortraitFailed: 'Не удалось сгенерировать портрет.',
|
||||
|
||||
modeOverview: 'Обзор',
|
||||
@@ -381,6 +386,11 @@ const en: Messages = {
|
||||
peoplePortraitGenerating: 'Generating…',
|
||||
peoplePortraitMissing: 'Not generated yet.',
|
||||
peoplePortraitUnavailable: 'SwarmUI is not configured on the server.',
|
||||
peoplePortraitSwarmDisabled: 'SwarmUI: not configured',
|
||||
peoplePortraitSwarmChecking: 'SwarmUI: checking…',
|
||||
peoplePortraitSwarmConnected: 'SwarmUI: connected',
|
||||
peoplePortraitSwarmDisconnected: 'SwarmUI: unreachable',
|
||||
peoplePortraitSwarmOffline: 'SwarmUI is unreachable — check that the service is running.',
|
||||
peoplePortraitFailed: 'Could not generate the portrait.',
|
||||
|
||||
modeOverview: 'Overview',
|
||||
|
||||
@@ -346,6 +346,8 @@ export interface GameStatus {
|
||||
readonly maxSchools: number;
|
||||
readonly connections: number;
|
||||
readonly swarmUiConfigured: boolean;
|
||||
/** null when SwarmUI is not configured on the server. */
|
||||
readonly swarmUiConnected: boolean | null;
|
||||
}
|
||||
|
||||
export async function fetchGameStatus(): Promise<GameStatus> {
|
||||
|
||||
@@ -643,6 +643,24 @@ body {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.people__swarm-status {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.people__swarm-status--ok {
|
||||
color: var(--ok, #2e7d32);
|
||||
}
|
||||
|
||||
.people__swarm-status--bad {
|
||||
color: var(--danger, #c62828);
|
||||
}
|
||||
|
||||
.people__swarm-status--pending,
|
||||
.people__swarm-status--off {
|
||||
color: var(--text-muted, #666);
|
||||
}
|
||||
|
||||
.people__card-name {
|
||||
margin: 0 0 2px;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -70,18 +70,13 @@ export class ApplicantsDialog {
|
||||
private busy = false;
|
||||
private cardTab: PersonCardTab = 'overview';
|
||||
private swarmConfigured = false;
|
||||
private swarmConnected: boolean | null = null;
|
||||
private painted: PersonCard | null = null;
|
||||
|
||||
constructor(private readonly options: ApplicantsDialogOptions) {
|
||||
this.staffing = options.staffing;
|
||||
this.selectedId = options.selectedId ?? null;
|
||||
void fetchGameStatus()
|
||||
.then((status) => {
|
||||
this.swarmConfigured = status.swarmUiConfigured;
|
||||
})
|
||||
.catch(() => {
|
||||
this.swarmConfigured = false;
|
||||
});
|
||||
void this.refreshSwarmStatus();
|
||||
this.error.hidden = true;
|
||||
this.ageMinInput.min = '0';
|
||||
this.ageMaxInput.min = '0';
|
||||
@@ -325,15 +320,33 @@ export class ApplicantsDialog {
|
||||
tab: this.cardTab,
|
||||
onTabChange: (tab) => {
|
||||
this.cardTab = tab;
|
||||
if (this.painted !== null) {
|
||||
if (tab === 'portrait') {
|
||||
void this.refreshSwarmStatus();
|
||||
} else if (this.painted !== null) {
|
||||
this.paintCard(this.painted);
|
||||
}
|
||||
},
|
||||
swarmConfigured: this.swarmConfigured,
|
||||
swarmConnected: this.swarmConnected,
|
||||
});
|
||||
this.appendHireIfNeeded(this.card);
|
||||
}
|
||||
|
||||
private async refreshSwarmStatus(): Promise<void> {
|
||||
try {
|
||||
const status = await fetchGameStatus();
|
||||
this.swarmConfigured = status.swarmUiConfigured;
|
||||
this.swarmConnected = status.swarmUiConnected;
|
||||
} catch {
|
||||
this.swarmConfigured = false;
|
||||
this.swarmConnected = null;
|
||||
}
|
||||
|
||||
if (this.painted !== null) {
|
||||
this.paintCard(this.painted);
|
||||
}
|
||||
}
|
||||
|
||||
private appendHireIfNeeded(parent: HTMLElement = this.card): void {
|
||||
const personId = this.selectedId ?? this.painted?.id;
|
||||
const applicant =
|
||||
|
||||
@@ -160,4 +160,17 @@ describe('renderPersonCard', () => {
|
||||
portraitTab?.click();
|
||||
expect(onTabChange).toHaveBeenCalledWith('portrait');
|
||||
});
|
||||
|
||||
it('shows Swarm connection status on the portrait tab', () => {
|
||||
setLocale('en');
|
||||
const root = document.createElement('div');
|
||||
renderPersonCard(root, card(), handlers({ tab: 'portrait', swarmConfigured: true, swarmConnected: true }));
|
||||
|
||||
expect(root.querySelector('.people__swarm-status--ok')?.textContent).toContain('connected');
|
||||
|
||||
root.replaceChildren();
|
||||
renderPersonCard(root, card(), handlers({ tab: 'portrait', swarmConfigured: true, swarmConnected: false }));
|
||||
|
||||
expect(root.querySelector('.people__swarm-status--bad')?.textContent).toContain('unreachable');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface PersonCardHandlers {
|
||||
readonly portraitBusy?: 'avatar' | 'full' | null;
|
||||
readonly portraitError?: string | null;
|
||||
readonly swarmConfigured?: boolean;
|
||||
/** null while checking or when SwarmUI is not configured. */
|
||||
readonly swarmConnected?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PersonCardMount {
|
||||
@@ -122,6 +124,8 @@ function mountOverview(parent: HTMLElement, card: PersonCard, handlers: PersonCa
|
||||
}
|
||||
|
||||
function mountPortrait(parent: HTMLElement, card: PersonCard, handlers: PersonCardHandlers): void {
|
||||
parent.append(swarmStatusLine(handlers));
|
||||
|
||||
parent.append(el('h4', { class: 'people__section-title', text: t('peoplePortraitAvatar') }));
|
||||
parent.append(portraitPreview(card, handlers, 'avatar'));
|
||||
parent.append(
|
||||
@@ -129,7 +133,7 @@ function mountPortrait(parent: HTMLElement, card: PersonCard, handlers: PersonCa
|
||||
class: 'button button--small',
|
||||
type: 'button',
|
||||
text: handlers.portraitBusy === 'avatar' ? t('peoplePortraitGenerating') : t('peoplePortraitGenerateAvatar'),
|
||||
disabled: handlers.portraitBusy !== null || handlers.swarmConfigured === false,
|
||||
disabled: handlers.portraitBusy !== null || !portraitGenerateEnabled(handlers),
|
||||
onClick: () => handlers.onGeneratePortrait?.('avatar'),
|
||||
}),
|
||||
);
|
||||
@@ -141,13 +145,15 @@ function mountPortrait(parent: HTMLElement, card: PersonCard, handlers: PersonCa
|
||||
class: 'button button--small',
|
||||
type: 'button',
|
||||
text: handlers.portraitBusy === 'full' ? t('peoplePortraitGenerating') : t('peoplePortraitGenerateFull'),
|
||||
disabled: handlers.portraitBusy !== null || handlers.swarmConfigured === false,
|
||||
disabled: handlers.portraitBusy !== null || !portraitGenerateEnabled(handlers),
|
||||
onClick: () => handlers.onGeneratePortrait?.('full'),
|
||||
}),
|
||||
);
|
||||
|
||||
if (handlers.swarmConfigured === false) {
|
||||
parent.append(el('p', { class: 'panel__empty', text: t('peoplePortraitUnavailable') }));
|
||||
} else if (handlers.swarmConfigured === true && handlers.swarmConnected === false) {
|
||||
parent.append(el('p', { class: 'panel__empty', text: t('peoplePortraitSwarmOffline') }));
|
||||
}
|
||||
|
||||
if ((handlers.portraitError?.length ?? 0) > 0) {
|
||||
@@ -155,6 +161,30 @@ function mountPortrait(parent: HTMLElement, card: PersonCard, handlers: PersonCa
|
||||
}
|
||||
}
|
||||
|
||||
function portraitGenerateEnabled(handlers: PersonCardHandlers): boolean {
|
||||
if (handlers.swarmConfigured !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return handlers.swarmConnected === true;
|
||||
}
|
||||
|
||||
function swarmStatusLine(handlers: PersonCardHandlers): HTMLElement {
|
||||
if (handlers.swarmConfigured === false) {
|
||||
return el('p', { class: 'people__swarm-status people__swarm-status--off', text: t('peoplePortraitSwarmDisabled') });
|
||||
}
|
||||
|
||||
if (handlers.swarmConnected === true) {
|
||||
return el('p', { class: 'people__swarm-status people__swarm-status--ok', text: t('peoplePortraitSwarmConnected') });
|
||||
}
|
||||
|
||||
if (handlers.swarmConnected === false) {
|
||||
return el('p', { class: 'people__swarm-status people__swarm-status--bad', text: t('peoplePortraitSwarmDisconnected') });
|
||||
}
|
||||
|
||||
return el('p', { class: 'people__swarm-status people__swarm-status--pending', text: t('peoplePortraitSwarmChecking') });
|
||||
}
|
||||
|
||||
function portraitPreview(card: PersonCard, handlers: PersonCardHandlers, kind: 'avatar' | 'full'): HTMLElement {
|
||||
const hasImage = kind === 'avatar' ? card.hasAvatar : card.hasFullBody;
|
||||
if (hasImage && handlers.schoolId !== null) {
|
||||
|
||||
@@ -15,6 +15,7 @@ export class PersonCardHost {
|
||||
private portraitBusy: 'avatar' | 'full' | null = null;
|
||||
private portraitError: string | null = null;
|
||||
private swarmConfigured = false;
|
||||
private swarmConnected: boolean | null = null;
|
||||
private schoolId: number | null = null;
|
||||
private painted: PersonCard | null = null;
|
||||
private mount: PersonCardMount | null = null;
|
||||
@@ -25,13 +26,20 @@ export class PersonCardHost {
|
||||
|
||||
attach(schoolId: number): void {
|
||||
this.schoolId = schoolId;
|
||||
void fetchGameStatus()
|
||||
.then((status) => {
|
||||
this.swarmConfigured = status.swarmUiConfigured;
|
||||
})
|
||||
.catch(() => {
|
||||
this.swarmConfigured = false;
|
||||
});
|
||||
void this.refreshSwarmStatus();
|
||||
}
|
||||
|
||||
private async refreshSwarmStatus(): Promise<void> {
|
||||
try {
|
||||
const status = await fetchGameStatus();
|
||||
this.swarmConfigured = status.swarmUiConfigured;
|
||||
this.swarmConnected = status.swarmUiConnected;
|
||||
} catch {
|
||||
this.swarmConfigured = false;
|
||||
this.swarmConnected = null;
|
||||
}
|
||||
|
||||
this.refreshPainted();
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
@@ -43,6 +51,7 @@ export class PersonCardHost {
|
||||
this.tab = 'overview';
|
||||
this.portraitBusy = null;
|
||||
this.portraitError = null;
|
||||
this.swarmConnected = null;
|
||||
}
|
||||
|
||||
paint(
|
||||
@@ -87,12 +96,17 @@ export class PersonCardHost {
|
||||
tab: this.tab,
|
||||
onTabChange: (tab) => {
|
||||
this.tab = tab;
|
||||
this.refreshPainted();
|
||||
if (tab === 'portrait') {
|
||||
void this.refreshSwarmStatus();
|
||||
} else {
|
||||
this.refreshPainted();
|
||||
}
|
||||
},
|
||||
onGeneratePortrait: (kind) => void this.generate(kind),
|
||||
portraitBusy: this.portraitBusy,
|
||||
portraitError: this.portraitError,
|
||||
swarmConfigured: this.swarmConfigured,
|
||||
swarmConnected: this.swarmConnected,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user