Refactors services with defaultConfig and polled data.
Much more simpler code base. Each service requires a poll functioon to poll its status. If type is defined, and the registered '+service.ts' exports a poll ServicePoller, it will be used. Otherwise, a generic poll mechanism is used.
This commit is contained in:
@@ -18,20 +18,17 @@ services:
|
|||||||
- title: '/Media'
|
- title: '/Media'
|
||||||
icon: 'fas fa-photo-film'
|
icon: 'fas fa-photo-film'
|
||||||
items:
|
items:
|
||||||
- title: 'Jellyfin'
|
- type: jellyfin
|
||||||
url: 'https://eagle.tuleu.me'
|
url: 'https://eagle.tuleu.me'
|
||||||
type: jellyfin
|
|
||||||
keywords: 'cloud storage files'
|
keywords: 'cloud storage files'
|
||||||
|
|
||||||
- title: 'Sonarr'
|
- type: sonarr
|
||||||
url: 'http://sonarr.lan'
|
url: 'http://sonarr.lan'
|
||||||
type: sonarr
|
|
||||||
api_key: 43f13770f9a0419bbdc3224dae76e886
|
api_key: 43f13770f9a0419bbdc3224dae76e886
|
||||||
keywords: 'shows tracker torrent usenet'
|
keywords: 'shows tracker torrent usenet'
|
||||||
|
|
||||||
- title: 'Radarr'
|
- type: radarr
|
||||||
url: 'http://radarr.lan'
|
url: 'http://radarr.lan'
|
||||||
type: radarr
|
|
||||||
keywords: 'movies tracker torrent usenet'
|
keywords: 'movies tracker torrent usenet'
|
||||||
|
|
||||||
- title: '/Cloud'
|
- title: '/Cloud'
|
||||||
@@ -46,8 +43,7 @@ services:
|
|||||||
- title: '/Infra'
|
- title: '/Infra'
|
||||||
icon: 'fas fa-network-wired'
|
icon: 'fas fa-network-wired'
|
||||||
items:
|
items:
|
||||||
- title: 'PiHole'
|
- type: pihole
|
||||||
url: 'http://pihole.lan/admin'
|
url: 'http://pihole.lan/admin'
|
||||||
type: 'pihole'
|
|
||||||
keywords: 'dns ads blocker internet'
|
keywords: 'dns ads blocker internet'
|
||||||
api_token: a3996b80e3d9cdb86b338396a164a8814e8d6f44d2986261fe573bfea53a75fb
|
api_token: a3996b80e3d9cdb86b338396a164a8814e8d6f44d2986261fe573bfea53a75fb
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { BrandConfig, ServiceConfig } from '$lib/config';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
import type { ServiceData } from '$lib/services/service';
|
import type { ServiceStatus } from '$lib/services/service';
|
||||||
import { getServiceComponent } from '$lib/services/services';
|
import { getServiceComponent } from '$lib/services/services';
|
||||||
import Brand from './Brand.svelte';
|
import Brand from './Brand.svelte';
|
||||||
|
|
||||||
export let service: ServiceConfig;
|
export let service: ServiceConfig;
|
||||||
export let data: ServiceData;
|
export let status: ServiceStatus | undefined;
|
||||||
|
export let data: Record<string, unknown>;
|
||||||
|
|
||||||
|
console.log(service);
|
||||||
|
|
||||||
const component = getServiceComponent(service.type || '');
|
const component = getServiceComponent(service.type || '');
|
||||||
|
|
||||||
function brand(): BrandConfig {
|
|
||||||
if (data?.logo != undefined && service.logo == undefined && service.icon == undefined) {
|
|
||||||
return { logo: data.logo, asmask: service.asmask || false };
|
|
||||||
}
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
|
|
||||||
function subtitle(): string {
|
|
||||||
return service.subtitle || data.subtitle || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeClasses(): string {
|
function computeClasses(): string {
|
||||||
switch (data.status) {
|
switch (status) {
|
||||||
case 'offline':
|
case 'offline':
|
||||||
return 'bg-error-400';
|
return 'bg-error-400';
|
||||||
case 'online':
|
case 'online':
|
||||||
@@ -36,16 +28,16 @@
|
|||||||
<a
|
<a
|
||||||
class="min-h-24 flex w-full flex-col overflow-hidden rounded-2xl bg-neutral-600 bg-opacity-30 p-0 transition-all hover:-translate-y-2 focus:-translate-y-2 focus:outline-none"
|
class="min-h-24 flex w-full flex-col overflow-hidden rounded-2xl bg-neutral-600 bg-opacity-30 p-0 transition-all hover:-translate-y-2 focus:-translate-y-2 focus:outline-none"
|
||||||
href={service.url}
|
href={service.url}
|
||||||
target={service.target || ''}
|
target={service.target || '_blank'}
|
||||||
>
|
>
|
||||||
<div class="flex flex-row gap-2 px-6 pt-6">
|
<div class="flex flex-row gap-2 px-6 pt-6">
|
||||||
<div class="flex flex h-12 w-12 flex-row text-4xl">
|
<div class="flex flex h-12 w-12 flex-row text-4xl">
|
||||||
<Brand brand={brand()} />
|
<Brand brand={service} />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-12">
|
<div class="h-12">
|
||||||
<p class="text-lg">{service.title}</p>
|
<p class="text-lg">{service.title || ''}</p>
|
||||||
<p class="min-h-12 text-neutral-700 dark:text-neutral-500">
|
<p class="min-h-12 text-neutral-700 dark:text-neutral-500">
|
||||||
{subtitle()}
|
{service.subtitle || ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ServiceGroupConfig } from '$lib/config';
|
import type { ServiceGroupConfig } from '$lib/config';
|
||||||
import { layoutDirection } from '$lib/stores/layout';
|
import type { ServiceData } from '$lib/services/service';
|
||||||
|
import { layoutDirection, type LayoutDirection } from '$lib/stores/layout';
|
||||||
import ServiceCard from './ServiceCard.svelte';
|
import ServiceCard from './ServiceCard.svelte';
|
||||||
|
|
||||||
export let group: ServiceGroupConfig;
|
export let group: ServiceGroupConfig;
|
||||||
export let groupData: Array<unknown>;
|
export let groupData: Array<Partial<ServiceData>>;
|
||||||
export let columns: number;
|
export let columns: number;
|
||||||
|
|
||||||
function renderClasses(columns: number, direction: LayoutDirection): string {
|
function renderClasses(columns: number, direction: LayoutDirection): string {
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
|
|
||||||
<div class="gap-4 {layoutClasses}">
|
<div class="gap-4 {layoutClasses}">
|
||||||
{#each group.items as service, i}
|
{#each group.items as service, i}
|
||||||
<ServiceCard {service} data={groupData[i]} />
|
<ServiceCard {service} data={groupData[i]?.data || {}} status={groupData[i].status} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
|
|
||||||
import * as ipRangeCheck from 'ip-range-check';
|
import * as ipRangeCheck from 'ip-range-check';
|
||||||
import { initializeServiceData } from './serviceDataPolling';
|
import { initializeServiceData } from './serviceDataPolling';
|
||||||
|
import { getServiceRecord } from '$lib/services/services';
|
||||||
|
|
||||||
const requiredService: Required<ServiceConfig> = {
|
const requiredService: Required<ServiceConfig> = {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -86,8 +87,29 @@ function merge<Type extends SPOJO>(a: Type, b: SPOJO): Type {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function injectDefaultServiceConfigs(config: Config) {
|
||||||
|
for (const group of config.services.values()) {
|
||||||
|
for (const service of group.items.values()) {
|
||||||
|
if (!service.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const def = getServiceRecord(service.type).config;
|
||||||
|
for (const [key, value] of Object.entries(def)) {
|
||||||
|
const isUnknownKey = !(key in requiredService);
|
||||||
|
const keyAlreadyDefined = key in service && service[key] != false;
|
||||||
|
if (isUnknownKey || keyAlreadyDefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
service[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function mergeConfig(a: Config, b: SPOJO): Config {
|
export function mergeConfig(a: Config, b: SPOJO): Config {
|
||||||
return merge<Config>(a, b);
|
const res = merge<Config>(a, b);
|
||||||
|
injectDefaultServiceConfigs(res);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLightConfig: ColorConfig = {};
|
const defaultLightConfig: ColorConfig = {};
|
||||||
@@ -184,7 +206,7 @@ export function watchDymamicConfig() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const watcher = watch(__filepath);
|
const watcher = watch(__filepath);
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
for await (const event of watcher) {
|
for await (const event of watcher) {
|
||||||
reloadConfig();
|
reloadConfig();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,17 @@
|
|||||||
import type { Config, ServiceConfig } from '$lib/config';
|
import type { Config } from '$lib/config';
|
||||||
import type { AsyncServiceHandler, ServiceData, ServiceHandler } from '$lib/services/service';
|
import type { ServiceData, ServicePoller } from '$lib/services/service';
|
||||||
import { getServiceHandler } from '$lib/services/services';
|
import { getServiceRecord } from '$lib/services/services';
|
||||||
import { serverConfig } from './config';
|
import { serverConfig } from './config';
|
||||||
|
|
||||||
export const serviceData: Array<Array<unknown>> = [];
|
export const serviceData: Array<Array<Partial<ServiceData>>> = [];
|
||||||
|
|
||||||
function isPromise(p: any): boolean {
|
|
||||||
return typeof p === 'object' && typeof p.then === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollGeneric(upstream: ServiceData, url: string): Promise<ServiceData> {
|
|
||||||
upstream.status = undefined;
|
|
||||||
return fetch(url)
|
|
||||||
.then((resp: Response): ServiceData => {
|
|
||||||
upstream.status = resp.ok ? 'online' : 'offline';
|
|
||||||
return upstream;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn("could not fetch status for '" + url + "': " + error);
|
|
||||||
upstream.status = 'offline';
|
|
||||||
return upstream;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapHandler(handler: ServiceHandler): AsyncServiceHandler {
|
|
||||||
return async (config: ServiceConfig): Promise<ServiceData> => {
|
|
||||||
const value = handler(config);
|
|
||||||
if (isPromise(value) === true) {
|
|
||||||
return (value as Promise<ServiceData>)
|
|
||||||
.then((data: ServiceData) => {
|
|
||||||
if (data.status != undefined) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
return pollGeneric(data, config.url);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn("could not resolve service '" + config.url + "': " + error);
|
|
||||||
return pollGeneric({}, config.url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return pollGeneric(value as ServiceData, config.url);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollServices() {
|
function pollServices() {
|
||||||
const config: Config = serverConfig();
|
const config: Config = serverConfig();
|
||||||
|
|
||||||
for (const [i, group] of config.services.entries()) {
|
for (const [i, group] of config.services.entries()) {
|
||||||
for (const [j, service] of group.items.entries()) {
|
for (const [j, service] of group.items.entries()) {
|
||||||
const handler: ServiceHandler =
|
const poller: ServicePoller = getServiceRecord(service.type || '').poll;
|
||||||
getServiceHandler(service.type || '') ||
|
poller(service).then((data: ServiceData) => {
|
||||||
(() => {
|
|
||||||
return {} as ServiceData;
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapHandler(handler)(service).then((data: ServiceData) => {
|
|
||||||
serviceData[i][j] = data;
|
serviceData[i][j] = data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { ServiceHandler } from '../service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
|
||||||
export const handle: ServiceHandler = () => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
return {
|
title: 'Jellyfin',
|
||||||
logo: 'https://cdn.rawgit.com/jellyfin/jellyfin-ux/master/branding/SVG/icon-transparent.svg',
|
logo: 'https://cdn.rawgit.com/jellyfin/jellyfin-ux/master/branding/SVG/icon-transparent.svg',
|
||||||
subtitle: 'Media Server'
|
subtitle: 'Media Server'
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CircularProgressBar from '$lib/components/CircularProgressBar.svelte';
|
import CircularProgressBar from '$lib/components/CircularProgressBar.svelte';
|
||||||
import type { ServiceData } from '../service';
|
|
||||||
|
|
||||||
export let data: ServiceData;
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
$: missingClasses = data.data == undefined ? 'animate-pulse' : '';
|
export let data: Record<string, any>;
|
||||||
|
|
||||||
|
$: missingClasses =
|
||||||
|
'status' in data && 'ads_percentage' in data && 'queries_today' in data && 'clients' in data
|
||||||
|
? ''
|
||||||
|
: 'animate-pulse';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row justify-start gap-4 {missingClasses} whitespace-nowrap">
|
<div class="flex flex-row justify-start gap-4 {missingClasses} whitespace-nowrap">
|
||||||
<span class="">
|
<span class="">
|
||||||
{#if data.data?.status == 'enabled'}
|
{#if data?.status == 'enabled'}
|
||||||
<i class="fa fa-play text-success-400 opacity-70" />
|
<i class="fa fa-play text-success-400 opacity-70" />
|
||||||
{:else if data.data?.status == 'disabled'}
|
{:else if data?.status == 'disabled'}
|
||||||
<i class="fa fa-stop text-error-400 opacity-70" />
|
<i class="fa fa-stop text-error-400 opacity-70" />
|
||||||
{:else}
|
{:else}
|
||||||
<i class="fa fa-question" />
|
<i class="fa fa-question" />
|
||||||
@@ -19,13 +23,13 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span class="m-0 inline-block h-5 w-5 p-0 align-text-bottom">
|
<span class="m-0 inline-block h-5 w-5 p-0 align-text-bottom">
|
||||||
<CircularProgressBar ratio={(data.data?.ads_percentage || 0) / 100.0} />
|
<CircularProgressBar ratio={(data?.ads_percentage || 0) / 100.0} />
|
||||||
</span>
|
</span>
|
||||||
{data.data?.ads_percentage?.toFixed(1) || 'N.A.'}%
|
{(data?.ads_percentage || NaN).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i class="fa-solid fa-globe" />
|
<i class="fa-solid fa-globe" />
|
||||||
{data.data?.queries_today || 'N.A.'}
|
{data?.queries_today || 'N.A.'}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-dharmachakra" />
|
<i class="fa fa-dharmachakra" />
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import type { ServiceConfig } from '$lib/config';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
import { type ServiceData, type ServiceHandler } from '../service';
|
import type { ServicePoller } from '../service';
|
||||||
|
|
||||||
export const handle: ServiceHandler = async (config: ServiceConfig) => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
const res: ServiceData = {
|
title: 'Pi-hole',
|
||||||
logo: 'https://cdn.rawgit.com/pi-hole/graphics/master/Vortex/Vortex.svg',
|
logo: 'https://cdn.rawgit.com/pi-hole/graphics/master/Vortex/Vortex.svg',
|
||||||
subtitle: 'Sends ads into a black hole'
|
subtitle: 'Sends ads into a black hole'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const poll: ServicePoller = async (config: ServiceConfig) => {
|
||||||
try {
|
try {
|
||||||
const resp: Response = await fetch(
|
const resp: Response = await fetch(
|
||||||
config.url + '/api.php?summaryRaw&auth=' + config.api_token
|
config.url + '/api.php?summaryRaw&auth=' + config.api_token
|
||||||
);
|
);
|
||||||
res.status = resp.ok ? 'online' : 'offline';
|
|
||||||
const raw = await resp.json();
|
const raw = await resp.json();
|
||||||
res.data = {
|
return {
|
||||||
queries_today: raw.dns_queries_today,
|
status: resp.ok ? 'online' : 'offline',
|
||||||
ads_percentage: raw.ads_percentage_today,
|
data: {
|
||||||
status: raw.status,
|
queries_today: raw.dns_queries_today,
|
||||||
client: raw.unique_clients
|
ads_percentage: raw.ads_percentage_today,
|
||||||
|
status: raw.status,
|
||||||
|
client: raw.unique_clients
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status = 'offline';
|
|
||||||
console.warn('could not fetch pihole status: ' + error);
|
console.warn('could not fetch pihole status: ' + error);
|
||||||
|
return { status: 'offline' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { ServiceHandler } from '../service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
|
||||||
export const handle: ServiceHandler = () => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
return { logo: 'https://cdn.rawgit.com/Prowlarr/Prowlarr/develop/Logo/Prowlarr.svg' };
|
title: 'Prowlarr',
|
||||||
|
logo: 'https://cdn.rawgit.com/Prowlarr/Prowlarr/develop/Logo/Prowlarr.svg',
|
||||||
|
subtitle: 'Indexer of indexer'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { ServiceHandler } from '../service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
|
||||||
export const handle: ServiceHandler = () => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
return {
|
title: 'Sonarr',
|
||||||
logo: 'https://cdn.rawgit.com/Radarr/Radarr/develop/Logo/Radarr.svg',
|
logo: 'https://cdn.rawgit.com/Radarr/Radarr/develop/Logo/Radarr.svg',
|
||||||
subtitle: 'TV Shows Tracker'
|
subtitle: 'TV Shows Tracker'
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import type { ServiceConfig } from '$lib/config';
|
|||||||
export type ServiceStatus = 'online' | 'offline';
|
export type ServiceStatus = 'online' | 'offline';
|
||||||
|
|
||||||
export interface ServiceData {
|
export interface ServiceData {
|
||||||
status?: ServiceStatus;
|
status: ServiceStatus;
|
||||||
|
|
||||||
[x: string]: unknown;
|
data?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceHandler = (input: ServiceConfig) => ServiceData | Promise<ServiceData>;
|
export type ServicePoller = (input: ServiceConfig) => Promise<ServiceData>;
|
||||||
export type AsyncServiceHandler = (input: ServiceConfig) => Promise<ServiceData>;
|
|
||||||
|
|||||||
@@ -1,21 +1,45 @@
|
|||||||
import type { ServiceHandler } from './service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
import type { ServiceData, ServicePoller } from './service';
|
||||||
|
|
||||||
const services: Record<string, ServiceHandler> = {};
|
interface ServiceRecord {
|
||||||
|
poll: ServicePoller;
|
||||||
const components: Record<string, any> = {};
|
config: Partial<ServiceConfig>;
|
||||||
|
|
||||||
function registerService(type: string, handler: ServiceHandler) {
|
|
||||||
services[type] = handler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const services: Record<string, ServiceRecord> = {};
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const components: Record<string, any> = {};
|
||||||
|
|
||||||
|
async function pollURL(config: ServiceConfig): Promise<ServiceData> {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(config.url);
|
||||||
|
return { status: resp.ok ? 'online' : 'offline' };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`could not poll '${config.url}': ` + error);
|
||||||
|
return { status: 'offline' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerService(type: string, service: Partial<ServiceRecord>) {
|
||||||
|
const record: ServiceRecord = {
|
||||||
|
poll: service.poll || pollURL,
|
||||||
|
config: service.config || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
services[type] = record;
|
||||||
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function registerComponent(type: string, component: any) {
|
function registerComponent(type: string, component: any) {
|
||||||
components[type] = component;
|
components[type] = component;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServiceHandler(type: string): ServiceHandler | undefined {
|
export function getServiceRecord(type: string): ServiceRecord {
|
||||||
return services[type];
|
return services[type] || { poll: pollURL, config: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function getServiceComponent(type: string): any {
|
export function getServiceComponent(type: string): any {
|
||||||
return components[type];
|
return components[type];
|
||||||
}
|
}
|
||||||
@@ -25,12 +49,13 @@ export async function initServices() {
|
|||||||
|
|
||||||
for (const [modulePath, load] of Object.entries(services)) {
|
for (const [modulePath, load] of Object.entries(services)) {
|
||||||
try {
|
try {
|
||||||
const { handle } = (await load()) as any;
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (handle == undefined) {
|
const { poll, config } = (await load()) as any;
|
||||||
throw new Error(`${modulePath} does not export 'handle'`);
|
if (poll == undefined && config == undefined) {
|
||||||
|
throw new Error(`${modulePath} does not export 'poll' or 'config'`);
|
||||||
}
|
}
|
||||||
const typeName = modulePath.slice(18, -12);
|
const typeName = modulePath.slice(18, -12);
|
||||||
registerService(typeName, handle);
|
registerService(typeName, { poll, config });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Could not load service definition from '${modulePath}': ${err}`);
|
console.error(`Could not load service definition from '${modulePath}': ${err}`);
|
||||||
}
|
}
|
||||||
@@ -42,6 +67,7 @@ export async function initComponents() {
|
|||||||
|
|
||||||
for (const [componentPath, load] of Object.entries(services)) {
|
for (const [componentPath, load] of Object.entries(services)) {
|
||||||
try {
|
try {
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const module = (await load()) as any;
|
const module = (await load()) as any;
|
||||||
|
|
||||||
if (module == undefined) {
|
if (module == undefined) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { humanizeRelativeTime } from '$lib/humanize';
|
import { humanizeRelativeTime } from '$lib/humanize';
|
||||||
import type { ServiceData } from '../service';
|
|
||||||
|
|
||||||
export let data: ServiceData;
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export let data: Record<string, any>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row justify-start gap-4 whitespace-nowrap">
|
<div class="flex flex-row justify-start gap-4 whitespace-nowrap">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { ServiceConfig } from '$lib/config';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
import type { ServiceHandler } from '../service';
|
import type { ServicePoller, ServiceStatus } from '../service';
|
||||||
|
|
||||||
interface Status {
|
interface Status {
|
||||||
warnings: number;
|
warnings: number;
|
||||||
errors: number;
|
errors: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function buildStatus(statuses: any[]): Status {
|
function buildStatus(statuses: any[]): Status {
|
||||||
let warnings = 0;
|
let warnings = 0;
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
@@ -26,6 +27,8 @@ interface Queue {
|
|||||||
nextDate?: Date;
|
nextDate?: Date;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function recordEstimatedCompletionTime(record: any): number {
|
function recordEstimatedCompletionTime(record: any): number {
|
||||||
if (record.estimatedCompletionTime == undefined) {
|
if (record.estimatedCompletionTime == undefined) {
|
||||||
return Infinity;
|
return Infinity;
|
||||||
@@ -33,6 +36,7 @@ function recordEstimatedCompletionTime(record: any): number {
|
|||||||
return new Date(record.estimatedCompletionTime).getTime();
|
return new Date(record.estimatedCompletionTime).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function buildQueue(queue: any): Queue {
|
function buildQueue(queue: any): Queue {
|
||||||
if (queue?.records?.length === 0) {
|
if (queue?.records?.length === 0) {
|
||||||
return { total: 0 };
|
return { total: 0 };
|
||||||
@@ -51,12 +55,13 @@ function buildQueue(queue: any): Queue {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handle: ServiceHandler = async (config: ServiceConfig) => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
const res = {
|
title: 'Sonarr',
|
||||||
logo: 'https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/Sonarr.svg',
|
logo: 'https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/Sonarr.svg',
|
||||||
subtitle: 'TV Show tracker'
|
subtitle: 'TV Show tracker'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const poll: ServicePoller = async (config: ServiceConfig) => {
|
||||||
const params = '?apikey=' + config.api_key;
|
const params = '?apikey=' + config.api_key;
|
||||||
|
|
||||||
const requests = [
|
const requests = [
|
||||||
@@ -65,21 +70,22 @@ export const handle: ServiceHandler = async (config: ServiceConfig) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [health, queue] = await Promise.allSettled(requests);
|
const [health, queue] = await Promise.allSettled(requests);
|
||||||
res.status = 'online';
|
let status: ServiceStatus = 'online';
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (health.status != 'fulfilled') {
|
if (health.status != 'fulfilled') {
|
||||||
console.warn("Could not fetch '" + config.url + "' status: " + health.value);
|
console.warn("Could not fetch '" + config.url + "' status: " + health.reason);
|
||||||
res.status = 'offline';
|
status = 'offline';
|
||||||
} else if (health.value.ok == true) {
|
} else if (health.value.ok == true) {
|
||||||
res.health = buildStatus(await health.value.json());
|
data.health = buildStatus(await health.value.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queue.status != 'fulfilled') {
|
if (queue.status != 'fulfilled') {
|
||||||
console.warn("Could not fetch '" + config.url + "' queue: " + queue.value);
|
console.warn("Could not fetch '" + config.url + "' queue: " + queue.reason);
|
||||||
res.status = 'offline';
|
data.status = 'offline';
|
||||||
} else if (queue.value.ok == true) {
|
} else if (queue.value.ok == true) {
|
||||||
res.queue = buildQueue(await queue.value.json());
|
data.queue = buildQueue(await queue.value.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return { status, data };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
title: 'Hello World !!'
|
|
||||||
subtitle: 'actually, I am a new pilot.'
|
|
||||||
|
|
||||||
services:
|
|
||||||
- title: '/Cloud'
|
|
||||||
subtitle: 'Private Cloud Utilities'
|
|
||||||
icon: 'fas fa-cloud'
|
|
||||||
items:
|
|
||||||
- title: 'NAS'
|
|
||||||
subtitle: 'Network Attached Storage'
|
|
||||||
icon: 'fas fa-hard-drive'
|
|
||||||
target: '_blank'
|
|
||||||
url: '/NAS'
|
|
||||||
keywords: 'cloud storage files'
|
|
||||||
Reference in New Issue
Block a user