@appkit/llamacpp-cli 1.10.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +356 -3
  3. package/dist/cli.js +99 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/admin/config.d.ts +10 -0
  6. package/dist/commands/admin/config.d.ts.map +1 -0
  7. package/dist/commands/admin/config.js +100 -0
  8. package/dist/commands/admin/config.js.map +1 -0
  9. package/dist/commands/admin/logs.d.ts +10 -0
  10. package/dist/commands/admin/logs.d.ts.map +1 -0
  11. package/dist/commands/admin/logs.js +114 -0
  12. package/dist/commands/admin/logs.js.map +1 -0
  13. package/dist/commands/admin/restart.d.ts +2 -0
  14. package/dist/commands/admin/restart.d.ts.map +1 -0
  15. package/dist/commands/admin/restart.js +29 -0
  16. package/dist/commands/admin/restart.js.map +1 -0
  17. package/dist/commands/admin/start.d.ts +2 -0
  18. package/dist/commands/admin/start.d.ts.map +1 -0
  19. package/dist/commands/admin/start.js +30 -0
  20. package/dist/commands/admin/start.js.map +1 -0
  21. package/dist/commands/admin/status.d.ts +2 -0
  22. package/dist/commands/admin/status.d.ts.map +1 -0
  23. package/dist/commands/admin/status.js +82 -0
  24. package/dist/commands/admin/status.js.map +1 -0
  25. package/dist/commands/admin/stop.d.ts +2 -0
  26. package/dist/commands/admin/stop.d.ts.map +1 -0
  27. package/dist/commands/admin/stop.js +21 -0
  28. package/dist/commands/admin/stop.js.map +1 -0
  29. package/dist/commands/logs.d.ts +1 -0
  30. package/dist/commands/logs.d.ts.map +1 -1
  31. package/dist/commands/logs.js +22 -0
  32. package/dist/commands/logs.js.map +1 -1
  33. package/dist/commands/server-show.js +1 -1
  34. package/dist/commands/server-show.js.map +1 -1
  35. package/dist/lib/admin-manager.d.ts +111 -0
  36. package/dist/lib/admin-manager.d.ts.map +1 -0
  37. package/dist/lib/admin-manager.js +413 -0
  38. package/dist/lib/admin-manager.js.map +1 -0
  39. package/dist/lib/admin-server.d.ts +148 -0
  40. package/dist/lib/admin-server.d.ts.map +1 -0
  41. package/dist/lib/admin-server.js +1161 -0
  42. package/dist/lib/admin-server.js.map +1 -0
  43. package/dist/lib/download-job-manager.d.ts +64 -0
  44. package/dist/lib/download-job-manager.d.ts.map +1 -0
  45. package/dist/lib/download-job-manager.js +164 -0
  46. package/dist/lib/download-job-manager.js.map +1 -0
  47. package/dist/tui/MultiServerMonitorApp.js +2 -2
  48. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  49. package/dist/types/admin-config.d.ts +19 -0
  50. package/dist/types/admin-config.d.ts.map +1 -0
  51. package/dist/types/admin-config.js +3 -0
  52. package/dist/types/admin-config.js.map +1 -0
  53. package/dist/utils/format-utils.d.ts +6 -0
  54. package/dist/utils/format-utils.d.ts.map +1 -1
  55. package/dist/utils/format-utils.js +18 -0
  56. package/dist/utils/format-utils.js.map +1 -1
  57. package/dist/utils/log-parser.d.ts +9 -0
  58. package/dist/utils/log-parser.d.ts.map +1 -1
  59. package/dist/utils/log-parser.js +11 -0
  60. package/dist/utils/log-parser.js.map +1 -1
  61. package/docs/images/web-ui-servers.png +0 -0
  62. package/package.json +1 -1
  63. package/src/cli.ts +100 -0
  64. package/src/commands/admin/config.ts +121 -0
  65. package/src/commands/admin/logs.ts +91 -0
  66. package/src/commands/admin/restart.ts +26 -0
  67. package/src/commands/admin/start.ts +27 -0
  68. package/src/commands/admin/status.ts +84 -0
  69. package/src/commands/admin/stop.ts +16 -0
  70. package/src/commands/logs.ts +24 -0
  71. package/src/commands/server-show.ts +2 -2
  72. package/src/lib/admin-manager.ts +435 -0
  73. package/src/lib/admin-server.ts +1243 -0
  74. package/src/lib/download-job-manager.ts +213 -0
  75. package/src/tui/MultiServerMonitorApp.ts +3 -3
  76. package/src/types/admin-config.ts +25 -0
  77. package/src/utils/format-utils.ts +18 -0
  78. package/src/utils/log-parser.ts +13 -0
  79. package/web/README.md +429 -0
  80. package/web/eslint.config.js +23 -0
  81. package/web/index.html +13 -0
  82. package/web/llamacpp-web-dist.tar.gz +0 -0
  83. package/web/package-lock.json +4017 -0
  84. package/web/package.json +38 -0
  85. package/web/postcss.config.js +6 -0
  86. package/web/public/vite.svg +1 -0
  87. package/web/src/App.css +42 -0
  88. package/web/src/App.tsx +86 -0
  89. package/web/src/assets/react.svg +1 -0
  90. package/web/src/components/ApiKeyPrompt.tsx +71 -0
  91. package/web/src/components/CreateServerModal.tsx +372 -0
  92. package/web/src/components/DownloadProgress.tsx +123 -0
  93. package/web/src/components/Nav.tsx +89 -0
  94. package/web/src/components/RouterConfigModal.tsx +240 -0
  95. package/web/src/components/SearchModal.tsx +306 -0
  96. package/web/src/components/ServerConfigModal.tsx +291 -0
  97. package/web/src/hooks/useApi.ts +259 -0
  98. package/web/src/index.css +42 -0
  99. package/web/src/lib/api.ts +226 -0
  100. package/web/src/main.tsx +10 -0
  101. package/web/src/pages/Dashboard.tsx +103 -0
  102. package/web/src/pages/Models.tsx +258 -0
  103. package/web/src/pages/Router.tsx +270 -0
  104. package/web/src/pages/RouterLogs.tsx +201 -0
  105. package/web/src/pages/ServerLogs.tsx +553 -0
  106. package/web/src/pages/Servers.tsx +358 -0
  107. package/web/src/types/api.ts +140 -0
  108. package/web/tailwind.config.js +31 -0
  109. package/web/tsconfig.app.json +28 -0
  110. package/web/tsconfig.json +7 -0
  111. package/web/tsconfig.node.json +26 -0
  112. package/web/vite.config.ts +25 -0
  113. package/MONITORING-ACCURACY-FIX.md +0 -199
  114. package/PER-PROCESS-METRICS.md +0 -190
@@ -0,0 +1,259 @@
1
+ import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query';
2
+ import { api } from '../lib/api';
3
+ import type { CreateServerRequest, UpdateServerRequest, UpdateRouterRequest } from '../types/api';
4
+
5
+ // System
6
+ export function useSystemStatus() {
7
+ return useQuery({
8
+ queryKey: ['system', 'status'],
9
+ queryFn: () => api.getSystemStatus(),
10
+ refetchInterval: 5000, // Auto-refresh every 5s
11
+ });
12
+ }
13
+
14
+ // Servers
15
+ export function useServers() {
16
+ return useQuery({
17
+ queryKey: ['servers'],
18
+ queryFn: () => api.listServers(),
19
+ refetchInterval: 5000, // Auto-refresh every 5s
20
+ });
21
+ }
22
+
23
+ export function useServer(id: string) {
24
+ return useQuery({
25
+ queryKey: ['servers', id],
26
+ queryFn: () => api.getServer(id),
27
+ enabled: !!id,
28
+ });
29
+ }
30
+
31
+ export function useCreateServer() {
32
+ const queryClient = useQueryClient();
33
+
34
+ return useMutation({
35
+ mutationFn: (data: CreateServerRequest) => api.createServer(data),
36
+ onSuccess: () => {
37
+ queryClient.invalidateQueries({ queryKey: ['servers'] });
38
+ queryClient.invalidateQueries({ queryKey: ['system'] });
39
+ },
40
+ });
41
+ }
42
+
43
+ export function useUpdateServer() {
44
+ const queryClient = useQueryClient();
45
+
46
+ return useMutation({
47
+ mutationFn: ({ id, data }: { id: string; data: UpdateServerRequest }) =>
48
+ api.updateServer(id, data),
49
+ onSuccess: (_, variables) => {
50
+ queryClient.invalidateQueries({ queryKey: ['servers'] });
51
+ queryClient.invalidateQueries({ queryKey: ['servers', variables.id] });
52
+ },
53
+ });
54
+ }
55
+
56
+ export function useDeleteServer() {
57
+ const queryClient = useQueryClient();
58
+
59
+ return useMutation({
60
+ mutationFn: (id: string) => api.deleteServer(id),
61
+ onSuccess: () => {
62
+ queryClient.invalidateQueries({ queryKey: ['servers'] });
63
+ queryClient.invalidateQueries({ queryKey: ['system'] });
64
+ },
65
+ });
66
+ }
67
+
68
+ export function useStartServer() {
69
+ const queryClient = useQueryClient();
70
+
71
+ return useMutation({
72
+ mutationFn: (id: string) => api.startServer(id),
73
+ onSuccess: (_, id) => {
74
+ queryClient.invalidateQueries({ queryKey: ['servers'] });
75
+ queryClient.invalidateQueries({ queryKey: ['servers', id] });
76
+ },
77
+ });
78
+ }
79
+
80
+ export function useStopServer() {
81
+ const queryClient = useQueryClient();
82
+
83
+ return useMutation({
84
+ mutationFn: (id: string) => api.stopServer(id),
85
+ onSuccess: (_, id) => {
86
+ queryClient.invalidateQueries({ queryKey: ['servers'] });
87
+ queryClient.invalidateQueries({ queryKey: ['servers', id] });
88
+ },
89
+ });
90
+ }
91
+
92
+ export function useRestartServer() {
93
+ const queryClient = useQueryClient();
94
+
95
+ return useMutation({
96
+ mutationFn: (id: string) => api.restartServer(id),
97
+ onSuccess: (_, id) => {
98
+ queryClient.invalidateQueries({ queryKey: ['servers'] });
99
+ queryClient.invalidateQueries({ queryKey: ['servers', id] });
100
+ },
101
+ });
102
+ }
103
+
104
+ export function useServerLogs(serverId: string | null, lines = 500) {
105
+ return useQuery({
106
+ queryKey: ['serverLogs', serverId, lines],
107
+ queryFn: () => api.getServerLogs(serverId!, 'both', lines),
108
+ enabled: !!serverId,
109
+ refetchInterval: 2000, // Auto-refresh every 2s
110
+ });
111
+ }
112
+
113
+ // Models
114
+ export function useModels() {
115
+ return useQuery({
116
+ queryKey: ['models'],
117
+ queryFn: () => api.listModels(),
118
+ refetchInterval: 10000, // Auto-refresh every 10s
119
+ });
120
+ }
121
+
122
+ export function useModel(name: string) {
123
+ return useQuery({
124
+ queryKey: ['models', name],
125
+ queryFn: () => api.getModel(name),
126
+ enabled: !!name,
127
+ });
128
+ }
129
+
130
+ export function useDeleteModel() {
131
+ const queryClient = useQueryClient();
132
+
133
+ return useMutation({
134
+ mutationFn: ({ name, cascade }: { name: string; cascade: boolean }) =>
135
+ api.deleteModel(name, cascade),
136
+ onSuccess: () => {
137
+ queryClient.invalidateQueries({ queryKey: ['models'] });
138
+ queryClient.invalidateQueries({ queryKey: ['servers'] });
139
+ queryClient.invalidateQueries({ queryKey: ['system'] });
140
+ },
141
+ });
142
+ }
143
+
144
+ export function useDownloadModel() {
145
+ return useMutation({
146
+ mutationFn: ({ repo, filename }: { repo: string; filename: string }) =>
147
+ api.downloadModel(repo, filename),
148
+ // Don't invalidate immediately - download is background job
149
+ });
150
+ }
151
+
152
+ // Search
153
+ export function useSearchModels() {
154
+ return useMutation({
155
+ mutationFn: ({ query, limit = 20 }: { query: string; limit?: number }) =>
156
+ api.searchModels(query, limit),
157
+ });
158
+ }
159
+
160
+ export function useModelFiles(repoId: string | null) {
161
+ return useQuery({
162
+ queryKey: ['modelFiles', repoId],
163
+ queryFn: () => api.getModelFiles(repoId!),
164
+ enabled: !!repoId,
165
+ });
166
+ }
167
+
168
+ // Download Jobs
169
+ export function useDownloadJobs(enabled = true) {
170
+ return useQuery({
171
+ queryKey: ['downloadJobs'],
172
+ queryFn: () => api.listDownloadJobs(),
173
+ refetchInterval: enabled ? 1000 : false, // Poll every 1s when enabled
174
+ enabled,
175
+ });
176
+ }
177
+
178
+ export function useDownloadJob(jobId: string | null) {
179
+ return useQuery({
180
+ queryKey: ['downloadJobs', jobId],
181
+ queryFn: () => api.getDownloadJob(jobId!),
182
+ enabled: !!jobId,
183
+ refetchInterval: 500, // Fast polling for active job
184
+ });
185
+ }
186
+
187
+ export function useCancelDownload() {
188
+ const queryClient = useQueryClient();
189
+
190
+ return useMutation({
191
+ mutationFn: (jobId: string) => api.cancelDownloadJob(jobId),
192
+ onSuccess: () => {
193
+ queryClient.invalidateQueries({ queryKey: ['downloadJobs'] });
194
+ },
195
+ });
196
+ }
197
+
198
+ // Router
199
+ export function useRouter() {
200
+ return useQuery({
201
+ queryKey: ['router'],
202
+ queryFn: () => api.getRouter(),
203
+ refetchInterval: 5000, // Auto-refresh every 5s
204
+ placeholderData: keepPreviousData, // Keep previous data during refetch to prevent flash
205
+ });
206
+ }
207
+
208
+ export function useStartRouter() {
209
+ const queryClient = useQueryClient();
210
+
211
+ return useMutation({
212
+ mutationFn: () => api.startRouter(),
213
+ onSuccess: () => {
214
+ queryClient.invalidateQueries({ queryKey: ['router'] });
215
+ },
216
+ });
217
+ }
218
+
219
+ export function useStopRouter() {
220
+ const queryClient = useQueryClient();
221
+
222
+ return useMutation({
223
+ mutationFn: () => api.stopRouter(),
224
+ onSuccess: () => {
225
+ queryClient.invalidateQueries({ queryKey: ['router'] });
226
+ },
227
+ });
228
+ }
229
+
230
+ export function useRestartRouter() {
231
+ const queryClient = useQueryClient();
232
+
233
+ return useMutation({
234
+ mutationFn: () => api.restartRouter(),
235
+ onSuccess: () => {
236
+ queryClient.invalidateQueries({ queryKey: ['router'] });
237
+ },
238
+ });
239
+ }
240
+
241
+ export function useRouterLogs(lines = 500) {
242
+ return useQuery({
243
+ queryKey: ['routerLogs', lines],
244
+ queryFn: () => api.getRouterLogs('both', lines),
245
+ refetchInterval: 2000, // Auto-refresh every 2s
246
+ placeholderData: keepPreviousData, // Keep previous data during refetch to prevent flash
247
+ });
248
+ }
249
+
250
+ export function useUpdateRouter() {
251
+ const queryClient = useQueryClient();
252
+
253
+ return useMutation({
254
+ mutationFn: (data: UpdateRouterRequest) => api.updateRouter(data),
255
+ onSuccess: () => {
256
+ queryClient.invalidateQueries({ queryKey: ['router'] });
257
+ },
258
+ });
259
+ }
@@ -0,0 +1,42 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
5
+ line-height: 1.5;
6
+ font-weight: 400;
7
+ font-synthesis: none;
8
+ text-rendering: optimizeLegibility;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ min-width: 320px;
16
+ min-height: 100vh;
17
+ background-color: #fafafa;
18
+ color: #171717;
19
+ }
20
+
21
+ #root {
22
+ min-height: 100vh;
23
+ }
24
+
25
+ /* Custom scrollbar for Ollama aesthetic */
26
+ ::-webkit-scrollbar {
27
+ width: 8px;
28
+ height: 8px;
29
+ }
30
+
31
+ ::-webkit-scrollbar-track {
32
+ background: transparent;
33
+ }
34
+
35
+ ::-webkit-scrollbar-thumb {
36
+ background: #d4d4d4;
37
+ border-radius: 4px;
38
+ }
39
+
40
+ ::-webkit-scrollbar-thumb:hover {
41
+ background: #a3a3a3;
42
+ }
@@ -0,0 +1,226 @@
1
+ import type {
2
+ Server,
3
+ Model,
4
+ SystemStatus,
5
+ CreateServerRequest,
6
+ UpdateServerRequest,
7
+ ApiError,
8
+ HFModelResult,
9
+ DownloadJob,
10
+ RouterInfo,
11
+ UpdateRouterRequest,
12
+ } from '../types/api';
13
+
14
+ const API_BASE = ''; // Proxy handles routing
15
+
16
+ class ApiClient {
17
+ private apiKey: string | null = null;
18
+
19
+ setApiKey(key: string) {
20
+ this.apiKey = key;
21
+ localStorage.setItem('llamacpp_api_key', key);
22
+ }
23
+
24
+ getApiKey(): string | null {
25
+ if (!this.apiKey) {
26
+ this.apiKey = localStorage.getItem('llamacpp_api_key');
27
+ }
28
+ return this.apiKey;
29
+ }
30
+
31
+ clearApiKey() {
32
+ this.apiKey = null;
33
+ localStorage.removeItem('llamacpp_api_key');
34
+ }
35
+
36
+ private async request<T>(
37
+ endpoint: string,
38
+ options: RequestInit = {}
39
+ ): Promise<T> {
40
+ const apiKey = this.getApiKey();
41
+
42
+ const headers: Record<string, string> = {
43
+ 'Content-Type': 'application/json',
44
+ ...(options.headers as Record<string, string>),
45
+ };
46
+
47
+ if (apiKey && endpoint !== '/health') {
48
+ headers['Authorization'] = `Bearer ${apiKey}`;
49
+ }
50
+
51
+ const response = await fetch(`${API_BASE}${endpoint}`, {
52
+ ...options,
53
+ headers,
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const error: ApiError = await response.json();
58
+ throw new Error(error.details || error.error);
59
+ }
60
+
61
+ return response.json();
62
+ }
63
+
64
+ // Health
65
+ async getHealth() {
66
+ return this.request<{ status: string; uptime: number; timestamp: string }>('/health');
67
+ }
68
+
69
+ // System
70
+ async getSystemStatus() {
71
+ return this.request<SystemStatus>('/api/status');
72
+ }
73
+
74
+ // Servers
75
+ async listServers() {
76
+ return this.request<{ servers: Server[] }>('/api/servers');
77
+ }
78
+
79
+ async getServer(id: string) {
80
+ return this.request<{ server: Server }>(`/api/servers/${id}`);
81
+ }
82
+
83
+ async createServer(data: CreateServerRequest) {
84
+ return this.request<{ server: Server }>('/api/servers', {
85
+ method: 'POST',
86
+ body: JSON.stringify(data),
87
+ });
88
+ }
89
+
90
+ async updateServer(id: string, data: UpdateServerRequest) {
91
+ return this.request<{ server: Server }>(`/api/servers/${id}`, {
92
+ method: 'PATCH',
93
+ body: JSON.stringify(data),
94
+ });
95
+ }
96
+
97
+ async deleteServer(id: string) {
98
+ return this.request<{ success: boolean }>(`/api/servers/${id}`, {
99
+ method: 'DELETE',
100
+ });
101
+ }
102
+
103
+ async startServer(id: string) {
104
+ return this.request<{ server: Server }>(`/api/servers/${id}/start`, {
105
+ method: 'POST',
106
+ });
107
+ }
108
+
109
+ async stopServer(id: string) {
110
+ return this.request<{ server: Server }>(`/api/servers/${id}/stop`, {
111
+ method: 'POST',
112
+ });
113
+ }
114
+
115
+ async restartServer(id: string) {
116
+ return this.request<{ server: Server }>(`/api/servers/${id}/restart`, {
117
+ method: 'POST',
118
+ });
119
+ }
120
+
121
+ async getServerLogs(id: string, type: 'stdout' | 'stderr' | 'both' = 'both', lines = 100) {
122
+ return this.request<{ stdout: string; stderr: string }>(
123
+ `/api/servers/${id}/logs?type=${type}&lines=${lines}`
124
+ );
125
+ }
126
+
127
+ // Models
128
+ async listModels() {
129
+ return this.request<{ models: Model[] }>('/api/models');
130
+ }
131
+
132
+ async getModel(name: string) {
133
+ return this.request<{ model: Model }>(`/api/models/${encodeURIComponent(name)}`);
134
+ }
135
+
136
+ async searchModels(query: string, limit = 20) {
137
+ return this.request<{ results: HFModelResult[] }>(
138
+ `/api/models/search?q=${encodeURIComponent(query)}&limit=${limit}`
139
+ );
140
+ }
141
+
142
+ async getModelFiles(repoId: string) {
143
+ return this.request<{ repoId: string; files: string[] }>(
144
+ `/api/models/${encodeURIComponent(repoId)}/files`
145
+ );
146
+ }
147
+
148
+ async downloadModel(repo: string, filename: string) {
149
+ return this.request<{ jobId: string; status: string }>(
150
+ '/api/models/download',
151
+ {
152
+ method: 'POST',
153
+ body: JSON.stringify({ repo, filename }),
154
+ }
155
+ );
156
+ }
157
+
158
+ async deleteModel(name: string, cascade = false) {
159
+ return this.request<{ success: boolean; deletedServers?: string[] }>(
160
+ `/api/models/${encodeURIComponent(name)}?cascade=${cascade}`,
161
+ {
162
+ method: 'DELETE',
163
+ }
164
+ );
165
+ }
166
+
167
+ // Download Jobs
168
+ async listDownloadJobs() {
169
+ return this.request<{ jobs: DownloadJob[] }>('/api/jobs');
170
+ }
171
+
172
+ async getDownloadJob(jobId: string) {
173
+ return this.request<{ job: DownloadJob }>(`/api/jobs/${jobId}`);
174
+ }
175
+
176
+ async cancelDownloadJob(jobId: string) {
177
+ return this.request<{ success: boolean; message: string }>(
178
+ `/api/jobs/${jobId}`,
179
+ { method: 'DELETE' }
180
+ );
181
+ }
182
+
183
+ // Router
184
+ async getRouter() {
185
+ return this.request<RouterInfo>('/api/router');
186
+ }
187
+
188
+ async startRouter() {
189
+ return this.request<{ success: boolean; status: string; pid: number | null }>(
190
+ '/api/router/start',
191
+ { method: 'POST' }
192
+ );
193
+ }
194
+
195
+ async stopRouter() {
196
+ return this.request<{ success: boolean; status: string }>(
197
+ '/api/router/stop',
198
+ { method: 'POST' }
199
+ );
200
+ }
201
+
202
+ async restartRouter() {
203
+ return this.request<{ success: boolean; status: string; pid: number | null }>(
204
+ '/api/router/restart',
205
+ { method: 'POST' }
206
+ );
207
+ }
208
+
209
+ async getRouterLogs(type: 'stdout' | 'stderr' | 'both' = 'both', lines = 100) {
210
+ return this.request<{ stdout: string; stderr: string }>(
211
+ `/api/router/logs?type=${type}&lines=${lines}`
212
+ );
213
+ }
214
+
215
+ async updateRouter(data: UpdateRouterRequest) {
216
+ return this.request<{ success: boolean; needsRestart: boolean; config: any }>(
217
+ '/api/router',
218
+ {
219
+ method: 'PATCH',
220
+ body: JSON.stringify(data),
221
+ }
222
+ );
223
+ }
224
+ }
225
+
226
+ export const api = new ApiClient();
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,103 @@
1
+ import { useSystemStatus, useServers } from '../hooks/useApi';
2
+ import { Server, Box, Activity, Clock } from 'lucide-react';
3
+
4
+ export function Dashboard() {
5
+ const { data: status } = useSystemStatus();
6
+ const { data: serversData } = useServers();
7
+
8
+ const runningServers = serversData?.servers.filter(s => s.status === 'running') || [];
9
+
10
+ return (
11
+ <div className="max-w-7xl mx-auto px-6 py-8">
12
+ <div className="mb-8">
13
+ <h1 className="text-3xl font-semibold text-gray-900 dark:text-white mb-2">
14
+ Dashboard
15
+ </h1>
16
+ <p className="text-gray-600 dark:text-gray-400">
17
+ Manage your local llama.cpp servers
18
+ </p>
19
+ </div>
20
+
21
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
22
+ <div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6">
23
+ <div className="flex items-center justify-between mb-2">
24
+ <span className="text-sm text-gray-600 dark:text-gray-400">Total Servers</span>
25
+ <Server className="w-4 h-4 text-gray-400" />
26
+ </div>
27
+ <div className="text-3xl font-semibold text-gray-900 dark:text-white">
28
+ {status?.servers.total || 0}
29
+ </div>
30
+ </div>
31
+
32
+ <div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6">
33
+ <div className="flex items-center justify-between mb-2">
34
+ <span className="text-sm text-gray-600 dark:text-gray-400">Running</span>
35
+ <Activity className="w-4 h-4 text-green-500" />
36
+ </div>
37
+ <div className="text-3xl font-semibold text-green-600 dark:text-green-500">
38
+ {status?.servers.running || 0}
39
+ </div>
40
+ </div>
41
+
42
+ <div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6">
43
+ <div className="flex items-center justify-between mb-2">
44
+ <span className="text-sm text-gray-600 dark:text-gray-400">Stopped</span>
45
+ <Clock className="w-4 h-4 text-gray-400" />
46
+ </div>
47
+ <div className="text-3xl font-semibold text-gray-900 dark:text-white">
48
+ {status?.servers.stopped || 0}
49
+ </div>
50
+ </div>
51
+
52
+ <div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6">
53
+ <div className="flex items-center justify-between mb-2">
54
+ <span className="text-sm text-gray-600 dark:text-gray-400">Models</span>
55
+ <Box className="w-4 h-4 text-gray-400" />
56
+ </div>
57
+ <div className="text-3xl font-semibold text-gray-900 dark:text-white">
58
+ {status?.models.total || 0}
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg">
64
+ <div className="p-6 border-b border-gray-200 dark:border-gray-800">
65
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
66
+ Running Servers
67
+ </h2>
68
+ </div>
69
+ <div className="divide-y divide-gray-200 dark:divide-gray-800">
70
+ {runningServers.length === 0 ? (
71
+ <div className="p-8 text-center">
72
+ <p className="text-gray-500 dark:text-gray-400">No servers running</p>
73
+ </div>
74
+ ) : (
75
+ runningServers.map((server) => (
76
+ <div key={server.id} className="p-6 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
77
+ <div className="flex items-center justify-between">
78
+ <div>
79
+ <h3 className="text-sm font-medium text-gray-900 dark:text-white mb-1">
80
+ {server.modelName}
81
+ </h3>
82
+ <div className="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400">
83
+ <span>Port {server.port}</span>
84
+ <span>•</span>
85
+ <span>{server.threads} threads</span>
86
+ <span>•</span>
87
+ <span>Context {server.ctxSize}</span>
88
+ </div>
89
+ </div>
90
+ <div className="flex items-center space-x-2">
91
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
92
+ Running
93
+ </span>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ ))
98
+ )}
99
+ </div>
100
+ </div>
101
+ </div>
102
+ );
103
+ }