Skip to content

Commit

Permalink
feat: open media vault widget
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Feb 25, 2024
1 parent db25016 commit 1a5efb4
Show file tree
Hide file tree
Showing 13 changed files with 522 additions and 16 deletions.
14 changes: 0 additions & 14 deletions .env.example

This file was deleted.

2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"layout.manage.navigation.**",
],
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"explorer.fileNesting.patterns": {
Expand Down
35 changes: 35 additions & 0 deletions public/locales/en/modules/health-monitoring.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"descriptor": {
"name": "System Health Monitoring",
"description": "Information about your NAS",
"settings": {
"title": "System Health Monitoring",
"fahrenheit": {
"label": "Fahrenheit"
},
"cpu": {
"label": "CPU"
},
"memory": {
"label": "Memory"
},
"fileSystem": {
"label": "File System"
},
}
},
"info": {
"uptime": "Uptime",
"updates": "Updates",
"reboot": "Reboot",
"load": "Load Avg",
"totalMem": "Total memory",
"available": "Available",
},
"errors": {
"general": {
"title": "Unable to find your NAS",
"text": "There was a problem connecting to your NAS. Please verify your configuration/integration(s)."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,9 @@ export const availableIntegrations = [
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
label: 'Home Assistant',
},
{
value: 'openmediavault',
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/openmediavault.png',
label: 'OpenMediaVault',
},
] as const satisfies Readonly<SelectItem[]>;
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { inviteRouter } from './routers/invite/invite-router';
import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server';
import { notebookRouter } from './routers/notebook';
import { openmediavaultRouter } from './routers/openmediavault';
import { overseerrRouter } from './routers/overseerr';
import { passwordRouter } from './routers/password';
import { rssRouter } from './routers/rss';
Expand Down Expand Up @@ -49,6 +50,7 @@ export const rootRouter = createTRPCRouter({
password: passwordRouter,
notebook: notebookRouter,
smartHomeEntityState: smartHomeEntityStateRouter,
openmediavault: openmediavaultRouter,
});

// export type definition of API
Expand Down
119 changes: 119 additions & 0 deletions src/server/api/routers/openmediavault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import axios from 'axios';
import Consola from 'consola';
import { z } from 'zod';
import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';

import { createTRPCRouter, publicProcedure } from '../trpc';

let sessionId: string | null = null;
let loginToken: string | null = null;

async function makeOpenMediaVaultRPCCall(
serviceName: string,
method: string,
params: Record<string, any>,
headers: Record<string, string>,
input: { configName: string }
) {
const config = getConfig(input.configName);
const app = config.apps.find((app) => checkIntegrationsType(app.integration, ['openmediavault']));

if (!app) {
Consola.error(`App not found for configName '${input.configName}'`);
return null;
}

const appUrl = new URL(app.url);
const response = await axios.post(
`${appUrl.origin}/rpc.php`,
{
service: serviceName,
method: method,
params: params,
},
{
headers: {
'Content-Type': 'application/json',
...headers,
},
}
);
return response;
}

export const openmediavaultRouter = createTRPCRouter({
fetchData: publicProcedure
.input(
z.object({
configName: z.string(),
})
)
.query(async ({ input }) => {
let authResponse: any = null;
let app: any;

if (!sessionId || !loginToken) {
app = getConfig(input.configName)?.apps.find((app) =>
checkIntegrationsType(app.integration, ['openmediavault'])
);

if (!app) {
Consola.error(
`Failed to process request to app '${app.integration}' (${app.id}). Please check username & password`
);
return null;
}

authResponse = await makeOpenMediaVaultRPCCall(
'session',
'login',
{
username: findAppProperty(app, 'username'),
password: findAppProperty(app, 'password'),
},
{},
input
);

const cookies = authResponse.headers['set-cookie'] || [];
sessionId = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-SESSIONID'))
?.split(';')[0];
loginToken = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-LOGIN'))
?.split(';')[0];
}

const [systemInfoResponse, fileSystemResponse, cpuTempResponse] = await Promise.all([
makeOpenMediaVaultRPCCall(
'system',
'getInformation',
{},
{ Cookie: `${loginToken};${sessionId}` },
input
),
makeOpenMediaVaultRPCCall(
'filesystemmgmt',
'enumerateMountedFilesystems',
{ includeroot: true },
{ Cookie: `${loginToken};${sessionId}` },
input
),
makeOpenMediaVaultRPCCall(
'cputemp',
'get',
{},
{ Cookie: `${loginToken};${sessionId}` },
input
),
]);

return {
authenticated: authResponse ? authResponse.data.response.authenticated : true,
systemInfo: systemInfoResponse?.data.response,
fileSystem: fileSystemResponse?.data.response,
cpuTemp: cpuTempResponse?.data.response,
};
}),
});
1 change: 1 addition & 0 deletions src/tools/server/translation-namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const boardNamespaces = [
'modules/docker',
'modules/dashdot',
'modules/overseerr',
'modules/health-monitoring',
'modules/media-server',
'modules/indexer-manager',
'modules/common-media-cards',
Expand Down
4 changes: 3 additions & 1 deletion src/types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export type IntegrationType =
| 'nzbGet'
| 'pihole'
| 'adGuardHome'
| 'homeAssistant';
| 'homeAssistant'
| 'openmediavault';

export type AppIntegrationType = {
type: IntegrationType | null;
Expand Down Expand Up @@ -101,6 +102,7 @@ export const integrationFieldProperties: {
pihole: ['apiKey'],
adGuardHome: ['username', 'password'],
homeAssistant: ['apiKey'],
openmediavault: ['username', 'password'],
};

export type IntegrationFieldDefinitionType = {
Expand Down
118 changes: 118 additions & 0 deletions src/widgets/health-monitoring/HealthMonitoringCpu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Center, Flex, Group, HoverCard, RingProgress, Text } from '@mantine/core';
import { IconCpu } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';

const HealthMonitoringCpu = ({ info, cpuTemp, fahrenheit }: any) => {
const { t } = useTranslation('modules/health-monitoring');
const toFahrenheit = (value: number) => {
return Math.round(value * 1.8 + 32);
};

interface LoadDataItem {
label: string;
stats: number;
progress: number;
color: string;
}

const loadData = [
{
label: `${t('info.load')} (1min)`,
stats: info.loadAverage['1min'],
progress: info.loadAverage['1min'],
color: 'teal',
},
{
label: `${t('info.load')} (5min)`,
stats: info.loadAverage['5min'],
progress: info.loadAverage['5min'],
color: 'blue',
},
{
label: `${t('info.load')} (15min)`,
stats: info.loadAverage['15min'],
progress: info.loadAverage['15min'],
color: 'red',
},
] as const;

return (
<Group position="center">
<RingProgress
roundCaps
size={140}
thickness={12}
label={
<Center style={{ flexDirection: 'column' }}>
{info.cpuUtilization.toFixed(2)}%
<HoverCard width={280} shadow="md" position="top">
<HoverCard.Target>
<IconCpu size={40} />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text fz="lg" tt="uppercase" fw={700} c="dimmed" align="center">
{t('info.load')}
</Text>
<Flex
direction={{ base: 'column', sm: 'row' }}
gap={{ base: 'sm', sm: 'lg' }}
justify={{ sm: 'center' }}
>
{loadData.map((load: LoadDataItem) => (
<RingProgress
size={80}
roundCaps
thickness={8}
label={
<Text color={load.color} weight={700} align="center" size="xl">
{load.progress}
</Text>
}
sections={[{ value: load.progress, color: load.color, tooltip: load.label }]}
/>
))}
</Flex>
</HoverCard.Dropdown>
</HoverCard>
</Center>
}
sections={[
{
value: info.cpuUtilization.toFixed(2),
color: info.cpuUtilization.toFixed(2) > 70 ? 'red' : 'green',
},
]}
/>
<RingProgress
roundCaps
size={140}
thickness={12}
label={
<Center
style={{
flexDirection: 'column',
}}
>
{fahrenheit ? `${toFahrenheit(cpuTemp.cputemp)}°F` : `${cpuTemp.cputemp}°C`}
<IconCpu size={40} />
</Center>
}
sections={[
{
value: cpuTemp.cputemp,
color:
cpuTemp.cputemp < 35
? 'green'
: cpuTemp.cputemp > 35 && cpuTemp.cputemp < 60
? 'yellow'
: cpuTemp.cputemp > 60 && cpuTemp.cputemp < 70
? 'orange'
: 'red',
},
]}
/>
</Group>
);
};

export default HealthMonitoringCpu;
Loading

0 comments on commit 1a5efb4

Please sign in to comment.