Skip to content

Commit e0a390f

Browse files
committed
feat: add unraid api status manager
1 parent 0ee09ae commit e0a390f

File tree

7 files changed

+319
-1
lines changed

7 files changed

+319
-1
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Field, ObjectType } from '@nestjs/graphql';
2+
3+
@ObjectType()
4+
export class ApiStatusResponse {
5+
@Field(() => String, { description: 'Raw status output from unraid-api status command' })
6+
status: string;
7+
8+
@Field(() => Boolean, { description: 'Whether the API service is currently running' })
9+
isRunning: boolean;
10+
11+
@Field(() => String, { description: 'Timestamp of the status check' })
12+
timestamp: string;
13+
}
14+
15+
@ObjectType()
16+
export class RestartApiResponse {
17+
@Field(() => Boolean, { description: 'Whether the restart command was successful' })
18+
success: boolean;
19+
20+
@Field(() => String, { description: 'Response message from the restart command' })
21+
message: string;
22+
23+
@Field(() => String, { nullable: true, description: 'Error message if restart failed' })
24+
error?: string;
25+
26+
@Field(() => String, { description: 'Timestamp of the restart attempt' })
27+
timestamp: string;
28+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { Mutation, Query, Resolver } from '@nestjs/graphql';
3+
4+
import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';
5+
import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';
6+
import { execa } from 'execa';
7+
8+
import {
9+
ApiStatusResponse,
10+
RestartApiResponse,
11+
} from '@app/unraid-api/graph/resolvers/api-management/api-management.model.js';
12+
13+
@Injectable()
14+
@Resolver()
15+
export class ApiManagementResolver {
16+
private readonly logger = new Logger(ApiManagementResolver.name);
17+
18+
@Query(() => ApiStatusResponse, { description: 'Get the current API service status' })
19+
@UsePermissions({
20+
action: AuthAction.READ_ANY,
21+
resource: Resource.CONFIG,
22+
})
23+
async apiStatus(): Promise<ApiStatusResponse> {
24+
try {
25+
const { stdout } = await execa('unraid-api', ['status'], { shell: 'bash' });
26+
return {
27+
status: stdout,
28+
isRunning: stdout.includes('running') || stdout.includes('active'),
29+
timestamp: new Date().toISOString(),
30+
};
31+
} catch (error) {
32+
this.logger.error('Failed to get API status:', error);
33+
return {
34+
status: `Error: ${error.message}`,
35+
isRunning: false,
36+
timestamp: new Date().toISOString(),
37+
};
38+
}
39+
}
40+
41+
@Mutation(() => RestartApiResponse, { description: 'Restart the API service using rc.d script' })
42+
@UsePermissions({
43+
action: AuthAction.UPDATE_ANY,
44+
resource: Resource.CONFIG,
45+
})
46+
async restartApiService(): Promise<RestartApiResponse> {
47+
try {
48+
this.logger.log('Restarting API service via rc.d script');
49+
50+
const { stdout, stderr } = await execa('/etc/rc.d/rc.unraid-api', ['restart'], {
51+
shell: 'bash',
52+
timeout: 30000,
53+
});
54+
55+
return {
56+
success: true,
57+
message: stdout || 'API restart initiated successfully',
58+
error: stderr || null,
59+
timestamp: new Date().toISOString(),
60+
};
61+
} catch (error) {
62+
this.logger.error('Failed to restart API:', error);
63+
return {
64+
success: false,
65+
message: 'Failed to restart API service',
66+
error: error.message,
67+
timestamp: new Date().toISOString(),
68+
};
69+
}
70+
}
71+
}

plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,4 +600,8 @@ _(GraphQL API Developer Sandbox)_:
600600

601601
<!-- start unraid-api section -->
602602
<unraid-connect-settings></unraid-connect-settings>
603+
604+
<!-- API Status Manager -->
605+
<unraid-api-status-manager></unraid-api-status-manager>
606+
603607
<!-- end unraid-api section -->

plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/unraid-api.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function response_complete($httpcode, $result, $cli_success_msg='') {
3939
'start',
4040
'restart',
4141
'stop',
42+
'status',
4243
'report',
4344
'wanip'
4445
];
@@ -68,7 +69,12 @@ function response_complete($httpcode, $result, $cli_success_msg='') {
6869
response_complete(200, array('result' => $output), $output);
6970
break;
7071
case 'restart':
71-
exec('unraid-api restart 2>/dev/null', $output, $retval);
72+
exec('/etc/rc.d/rc.unraid-api restart 2>&1', $output, $retval);
73+
$output = implode(PHP_EOL, $output);
74+
response_complete(200, array('success' => ($retval === 0), 'result' => $output, 'error' => ($retval !== 0 ? $output : null)), $output);
75+
break;
76+
case 'status':
77+
exec('unraid-api status 2>&1', $output, $retval);
7278
$output = implode(PHP_EOL, $output);
7379
response_complete(200, array('result' => $output), $output);
7480
break;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script lang="ts">
2+
import ApiStatus from '@/components/ApiStatus/ApiStatus.vue';
3+
4+
export default ApiStatus;
5+
</script>
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<script lang="ts" setup>
2+
import { onMounted, ref } from 'vue';
3+
4+
import { toast } from 'sonner';
5+
6+
const apiStatus = ref<string>('');
7+
const isRunning = ref<boolean>(false);
8+
const isLoading = ref<boolean>(false);
9+
const isRestarting = ref<boolean>(false);
10+
11+
const checkStatus = async () => {
12+
isLoading.value = true;
13+
try {
14+
const response = await fetch('/plugins/dynamix.my.servers/include/unraid-api.php', {
15+
method: 'POST',
16+
headers: {
17+
'Content-Type': 'application/x-www-form-urlencoded',
18+
},
19+
body: 'command=status',
20+
});
21+
22+
const data = await response.json();
23+
if (data.result) {
24+
apiStatus.value = data.result;
25+
isRunning.value = data.result.includes('running') || data.result.includes('active');
26+
}
27+
} catch (error) {
28+
console.error('Failed to get API status:', error);
29+
apiStatus.value = 'Error fetching status';
30+
isRunning.value = false;
31+
} finally {
32+
isLoading.value = false;
33+
}
34+
};
35+
36+
const restartApi = async () => {
37+
const confirmed = window.confirm(
38+
'Are you sure you want to restart the Unraid API service? This will temporarily interrupt API connections.'
39+
);
40+
41+
if (!confirmed) return;
42+
43+
isRestarting.value = true;
44+
toast.info('Restarting API service...');
45+
46+
try {
47+
const response = await fetch('/plugins/dynamix.my.servers/include/unraid-api.php', {
48+
method: 'POST',
49+
headers: {
50+
'Content-Type': 'application/x-www-form-urlencoded',
51+
},
52+
body: 'command=restart',
53+
});
54+
55+
const data = await response.json();
56+
if (data.success) {
57+
toast.success('API service restart initiated. Please wait a few seconds.');
58+
setTimeout(() => {
59+
checkStatus();
60+
}, 3000);
61+
} else {
62+
toast.error(data.error || 'Failed to restart API service');
63+
}
64+
} catch (error) {
65+
console.error('Failed to restart API:', error);
66+
toast.error('Failed to restart API service');
67+
} finally {
68+
isRestarting.value = false;
69+
}
70+
};
71+
72+
onMounted(() => {
73+
checkStatus();
74+
});
75+
</script>
76+
77+
<template>
78+
<div class="api-status-container">
79+
<div class="api-status-header">
80+
<h3 class="mb-2 text-lg font-semibold">API Service Status</h3>
81+
<div class="status-indicator">
82+
<span class="status-label">Status:</span>
83+
<span :class="['status-value', isRunning ? 'text-green-500' : 'text-orange-500']">
84+
{{ isLoading ? 'Loading...' : isRunning ? 'Running' : 'Not Running' }}
85+
</span>
86+
</div>
87+
</div>
88+
89+
<div class="api-status-details">
90+
<pre class="status-output">{{ apiStatus }}</pre>
91+
</div>
92+
93+
<div class="api-status-actions">
94+
<button @click="checkStatus" :disabled="isLoading" class="btn btn-secondary">
95+
{{ isLoading ? 'Refreshing...' : 'Refresh Status' }}
96+
</button>
97+
<button @click="restartApi" :disabled="isRestarting" class="btn btn-danger">
98+
{{ isRestarting ? 'Restarting...' : 'Restart API' }}
99+
</button>
100+
</div>
101+
102+
<div class="api-status-help">
103+
<p class="mt-4 text-sm text-gray-600">
104+
View the current status of the Unraid API service and restart if needed. Use this to debug API
105+
connection issues.
106+
</p>
107+
</div>
108+
</div>
109+
</template>
110+
111+
<style scoped>
112+
.api-status-container {
113+
background-color: var(--background-secondary);
114+
border-radius: 8px;
115+
padding: 1.5rem;
116+
margin: 1rem 0;
117+
}
118+
119+
.api-status-header {
120+
margin-bottom: 1rem;
121+
}
122+
123+
.status-indicator {
124+
display: flex;
125+
align-items: center;
126+
gap: 0.5rem;
127+
font-size: 0.95rem;
128+
}
129+
130+
.status-label {
131+
font-weight: 500;
132+
}
133+
134+
.status-value {
135+
font-weight: 600;
136+
}
137+
138+
.api-status-details {
139+
margin: 1rem 0;
140+
}
141+
142+
.status-output {
143+
background-color: #1a1a1a;
144+
color: #e0e0e0;
145+
padding: 1rem;
146+
border-radius: 4px;
147+
font-size: 0.85rem;
148+
max-height: 200px;
149+
overflow-y: auto;
150+
white-space: pre-wrap;
151+
word-wrap: break-word;
152+
font-family: 'Courier New', monospace;
153+
}
154+
155+
.api-status-actions {
156+
display: flex;
157+
gap: 1rem;
158+
margin-top: 1rem;
159+
}
160+
161+
.btn {
162+
padding: 0.5rem 1rem;
163+
border-radius: 4px;
164+
border: none;
165+
cursor: pointer;
166+
font-size: 0.9rem;
167+
font-weight: 500;
168+
transition: opacity 0.2s;
169+
}
170+
171+
.btn:disabled {
172+
opacity: 0.6;
173+
cursor: not-allowed;
174+
}
175+
176+
.btn-secondary {
177+
background-color: #4a5568;
178+
color: white;
179+
}
180+
181+
.btn-secondary:hover:not(:disabled) {
182+
background-color: #2d3748;
183+
}
184+
185+
.btn-danger {
186+
background-color: #e53e3e;
187+
color: white;
188+
}
189+
190+
.btn-danger:hover:not(:disabled) {
191+
background-color: #c53030;
192+
}
193+
194+
.api-status-help {
195+
border-top: 1px solid var(--border-color);
196+
padding-top: 1rem;
197+
margin-top: 1.5rem;
198+
}
199+
</style>

web/src/components/Wrapper/component-registry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,9 @@ export const componentMappings: ComponentMapping[] = [
141141
selector: 'unraid-test-theme-switcher',
142142
appId: 'test-theme-switcher',
143143
},
144+
{
145+
component: defineAsyncComponent(() => import('../ApiStatus/ApiStatus.standalone.vue')),
146+
selector: 'unraid-api-status-manager',
147+
appId: 'api-status-manager',
148+
},
144149
];

0 commit comments

Comments
 (0)