Skip to content

Commit

Permalink
feat: horde heartbeat and worker messages (#57)
Browse files Browse the repository at this point in the history
* feat: implement network status check and alert

* feat: fetch worker messages

feat: fetch worker messages

* feat: messages

* chore: add read worker messages to dexie

* feat: handle read unread messages

* feat: implement wrapper for headernav buttons

* fix: issue with message read button

* chore: add titles to buttons

* chore: hide messages for users with no workers

* chore: pass in worker none

* fix: github links
  • Loading branch information
daveschumaker authored Jan 29, 2025
1 parent 1ae9203 commit 51676e4
Show file tree
Hide file tree
Showing 28 changed files with 703 additions and 240 deletions.
20 changes: 20 additions & 0 deletions app/_api/horde/heartbeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default async function hordeHeartbeat(): Promise<boolean> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout

const response = await fetch(
'https://aihorde.net/api/v2/status/heartbeat',
{
signal: controller.signal
}
);

clearTimeout(timeoutId);

return response.ok;
} catch (error) {
// Return false if there's a timeout, network error, or any other issue
return false;
}
}
25 changes: 25 additions & 0 deletions app/_api/horde/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AppConstants } from '@/app/_data-models/AppConstants';
import { AppSettings } from '@/app/_data-models/AppSettings';
import { clientHeader } from '@/app/_data-models/ClientHeader';
import { updateHordeMessages } from '@/app/_stores/UserStore';
import { WorkerMessage } from '@/app/_types/HordeTypes';

export const getWorkerMessages = async () => {
try {
const response = await fetch(
`${AppConstants.AI_HORDE_PROD_URL}/api/v2/workers/messages`,
{
headers: {
apikey: AppSettings.get('apiKey'),
'Content-Type': 'application/json',
'Client-Agent': clientHeader()
}
}
);

const data: WorkerMessage[] = await response.json();
updateHordeMessages(data);
} catch (error) {
console.error('Error fetching messages:', error);
}
};
30 changes: 24 additions & 6 deletions app/_components/AppInit/AppInitComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ import { useRouter, useSearchParams } from 'next/navigation';
import {
AppStore,
setAppBuildId,
setAppOnlineStatus
setAppOnlineStatus,
setHordeOnlineStatus
} from '@/app/_stores/AppStore';
import { appBasepath } from '@/app/_utils/browserUtils';
import { loadPendingImagesFromDexie } from '@/app/_controllers/pendingJobs/loadPendingImages';
import { initJobController } from '@/app/_controllers/pendingJobs';
import { toastController } from '@/app/_controllers/toastController';
import { updateUseSharedKey } from '@/app/_stores/UserStore';
import hordeHeartbeat from '@/app/_api/horde/heartbeat';
import { getWorkerMessages } from '@/app/_api/horde/messages';

export default function AppInitComponent({
modelsAvailable,
Expand All @@ -41,7 +44,7 @@ export default function AppInitComponent({

const [handleLogin] = useHordeApiKey();

const initHeartbeat = async () => {
const initArtbotHeartbeat = async () => {
try {
const res = await fetch(`${appBasepath()}/api/heartbeat`);
const { success, buildId } = (await res.json()) || {};
Expand All @@ -66,6 +69,15 @@ export default function AppInitComponent({
}
};

const initHordeHeartbeat = async () => {
const hordeOnline = await hordeHeartbeat();
setHordeOnlineStatus(hordeOnline);
};

const initHordeMessages = async () => {
await getWorkerMessages();
};

const getUserInfoOnLoad = async () => {
const apikey = AppSettings.apikey();

Expand All @@ -81,18 +93,24 @@ export default function AppInitComponent({
}, [modelDetails, modelsAvailable]);

useEffectOnce(() => {
console.log(`ArtBot v2.0.0_beta is online: ${new Date().toLocaleString()}`);

initDexie();
getUserInfoOnLoad();
loadPendingImagesFromDexie();
initJobController();

initHeartbeat();
const intervalHeartbeat = setInterval(initHeartbeat, 15 * 1000);
initArtbotHeartbeat();
const intervalHeartbeat = setInterval(initArtbotHeartbeat, 15 * 1000);

initHordeHeartbeat();
const intervalHordeHeartbeat = setInterval(initHordeHeartbeat, 15 * 1000);

initHordeMessages();
const intervalHordeMessages = setInterval(initHordeMessages, 60 * 1000);

return () => {
clearInterval(intervalHeartbeat);
clearInterval(intervalHordeHeartbeat);
clearInterval(intervalHordeMessages);
};
});

Expand Down
4 changes: 2 additions & 2 deletions app/_components/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export default function Footer() {
</div>
<div>
<Link
href="https://github.com/daveschumaker/artbot-for-stable-diffusion/issues"
href="https://github.com/Haidra-Org/artbot/issues"
target="_blank"
rel="noopener noreferrer"
>
Expand Down Expand Up @@ -245,7 +245,7 @@ export default function Footer() {
</div> */}
<div>
<Link
href="https://github.com/daveschumaker/artbot-for-stable-diffusion"
href="https://github.com/Haidra-Org/artbot"
target="_blank"
rel="noopener noreferrer"
>
Expand Down
31 changes: 31 additions & 0 deletions app/_components/HeaderNav/HeaderNav_ArtBotOffline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import NiceModal from '@ebay/nice-modal-react';
import HeaderNav_IconWrapper from './_HeaderNav_IconWrapper';
import { IconWifiOff } from '@tabler/icons-react';
import { useStore } from 'statery';
import { AppStore } from '@/app/_stores/AppStore';

export default function HeaderNavArtBotOffline() {
const { online } = useStore(AppStore);

if (online) return null;

return (
<HeaderNav_IconWrapper
onClick={() => {
NiceModal.show('modal', {
children: (
<div className="col">
<h2 className="row font-bold">Connection error</h2>
ArtBot is currently having trouble conecting to its server. You
may encounter unexpected errors.
</div>
),
modalClassName: 'max-w-[640px]'
});
}}
title="ArtBot server is currently offline."
>
<IconWifiOff color="orange" stroke={1.5} size={22} />
</HeaderNav_IconWrapper>
);
}
27 changes: 27 additions & 0 deletions app/_components/HeaderNav/HeaderNav_ForceWorker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { UserStore } from '@/app/_stores/UserStore';
import ForceWorkerModal from '@/app/create/_component/ForceWorkerModal';
import NiceModal from '@ebay/nice-modal-react';
import { IconAlertTriangleFilled } from '@tabler/icons-react';
import { useStore } from 'statery';
import HeaderNav_IconWrapper from './_HeaderNav_IconWrapper';

export default function HeaderNavForceWorker() {
const { forceSelectedWorker } = useStore(UserStore);
if (!forceSelectedWorker) return null;

return (
<HeaderNav_IconWrapper
onClick={() => {
NiceModal.show('modal', {
children: <ForceWorkerModal />,
modalClassName: 'max-w-[640px]'
});
}}
title="Requests locked to specific worker(s)"
>
<IconAlertTriangleFilled color="orange" stroke={1} size={22} />
</HeaderNav_IconWrapper>
);
}
52 changes: 52 additions & 0 deletions app/_components/HeaderNav/HeaderNav_HordeOffline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import NiceModal from '@ebay/nice-modal-react';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useStore } from 'statery';
import { AppStore } from '@/app/_stores/AppStore';
import HeaderNav_IconWrapper from './_HeaderNav_IconWrapper';

const HordeOfflineModal = () => {
return (
<div
className="flex flex-col gap-2"
style={{ maxWidth: '400px', width: '100%' }}
>
<div className="stats bg-body-color">
<div className="stat">
<div className="font-bold pb-1 flex flex-row items-center gap-2">
<IconAlertTriangle color="red" stroke={1.5} size={24} />
AI Horde offline
</div>
<div className="col gap-2">
<div className="text-secondary">
ArtBot has encountered an issue while attempting to contact the AI
Horde API. It may potentially be down. Network requests could be
affected.
</div>
<div className="text-secondary">Please check again soon.</div>
</div>
</div>
</div>
</div>
);
};

export default function HeaderNav_HordeOffline() {
const { hordeOnline } = useStore(AppStore);

if (hordeOnline) {
return null;
}

return (
<HeaderNav_IconWrapper
onClick={() => {
NiceModal.show('modal', {
children: <HordeOfflineModal />
});
}}
title="AI Horde is offline"
>
<IconAlertTriangle color="red" stroke={1.5} size={24} />
</HeaderNav_IconWrapper>
);
}
7 changes: 4 additions & 3 deletions app/_components/HeaderNav/HeaderNav_HordePerformance.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import NiceModal from '@ebay/nice-modal-react';
import { IconDeviceDesktopAnalytics } from '@tabler/icons-react';
import HordePerformanceModal from '../Modals/Modal_HordePerformance';
import HeaderNav_IconWrapper from './_HeaderNav_IconWrapper';

export default function HeaderNavHordePerformance() {
return (
<button
className="row text-xs text-black dark:text-white"
<HeaderNav_IconWrapper
onClick={() => {
NiceModal.show('hordePerfModal', {
children: <HordePerformanceModal />
});
}}
title="AI Horde performance"
>
<IconDeviceDesktopAnalytics stroke={1} size={22} />
</button>
</HeaderNav_IconWrapper>
);
}
Loading

0 comments on commit 51676e4

Please sign in to comment.