@appkit/llamacpp-cli 1.11.0 → 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.
- package/CHANGELOG.md +10 -0
- package/README.md +356 -3
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/admin/config.d.ts +10 -0
- package/dist/commands/admin/config.d.ts.map +1 -0
- package/dist/commands/admin/config.js +100 -0
- package/dist/commands/admin/config.js.map +1 -0
- package/dist/commands/admin/logs.d.ts +10 -0
- package/dist/commands/admin/logs.d.ts.map +1 -0
- package/dist/commands/admin/logs.js +114 -0
- package/dist/commands/admin/logs.js.map +1 -0
- package/dist/commands/admin/restart.d.ts +2 -0
- package/dist/commands/admin/restart.d.ts.map +1 -0
- package/dist/commands/admin/restart.js +29 -0
- package/dist/commands/admin/restart.js.map +1 -0
- package/dist/commands/admin/start.d.ts +2 -0
- package/dist/commands/admin/start.d.ts.map +1 -0
- package/dist/commands/admin/start.js +30 -0
- package/dist/commands/admin/start.js.map +1 -0
- package/dist/commands/admin/status.d.ts +2 -0
- package/dist/commands/admin/status.d.ts.map +1 -0
- package/dist/commands/admin/status.js +82 -0
- package/dist/commands/admin/status.js.map +1 -0
- package/dist/commands/admin/stop.d.ts +2 -0
- package/dist/commands/admin/stop.d.ts.map +1 -0
- package/dist/commands/admin/stop.js +21 -0
- package/dist/commands/admin/stop.js.map +1 -0
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +22 -0
- package/dist/commands/logs.js.map +1 -1
- package/dist/lib/admin-manager.d.ts +111 -0
- package/dist/lib/admin-manager.d.ts.map +1 -0
- package/dist/lib/admin-manager.js +413 -0
- package/dist/lib/admin-manager.js.map +1 -0
- package/dist/lib/admin-server.d.ts +148 -0
- package/dist/lib/admin-server.d.ts.map +1 -0
- package/dist/lib/admin-server.js +1161 -0
- package/dist/lib/admin-server.js.map +1 -0
- package/dist/lib/download-job-manager.d.ts +64 -0
- package/dist/lib/download-job-manager.d.ts.map +1 -0
- package/dist/lib/download-job-manager.js +164 -0
- package/dist/lib/download-job-manager.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.js +1 -1
- package/dist/types/admin-config.d.ts +19 -0
- package/dist/types/admin-config.d.ts.map +1 -0
- package/dist/types/admin-config.js +3 -0
- package/dist/types/admin-config.js.map +1 -0
- package/dist/utils/log-parser.d.ts +9 -0
- package/dist/utils/log-parser.d.ts.map +1 -1
- package/dist/utils/log-parser.js +11 -0
- package/dist/utils/log-parser.js.map +1 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/package.json +1 -1
- package/src/cli.ts +100 -0
- package/src/commands/admin/config.ts +121 -0
- package/src/commands/admin/logs.ts +91 -0
- package/src/commands/admin/restart.ts +26 -0
- package/src/commands/admin/start.ts +27 -0
- package/src/commands/admin/status.ts +84 -0
- package/src/commands/admin/stop.ts +16 -0
- package/src/commands/logs.ts +24 -0
- package/src/lib/admin-manager.ts +435 -0
- package/src/lib/admin-server.ts +1243 -0
- package/src/lib/download-job-manager.ts +213 -0
- package/src/tui/MultiServerMonitorApp.ts +1 -1
- package/src/types/admin-config.ts +25 -0
- package/src/utils/log-parser.ts +13 -0
- package/web/README.md +429 -0
- package/web/eslint.config.js +23 -0
- package/web/index.html +13 -0
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +4017 -0
- package/web/package.json +38 -0
- package/web/postcss.config.js +6 -0
- package/web/public/vite.svg +1 -0
- package/web/src/App.css +42 -0
- package/web/src/App.tsx +86 -0
- package/web/src/assets/react.svg +1 -0
- package/web/src/components/ApiKeyPrompt.tsx +71 -0
- package/web/src/components/CreateServerModal.tsx +372 -0
- package/web/src/components/DownloadProgress.tsx +123 -0
- package/web/src/components/Nav.tsx +89 -0
- package/web/src/components/RouterConfigModal.tsx +240 -0
- package/web/src/components/SearchModal.tsx +306 -0
- package/web/src/components/ServerConfigModal.tsx +291 -0
- package/web/src/hooks/useApi.ts +259 -0
- package/web/src/index.css +42 -0
- package/web/src/lib/api.ts +226 -0
- package/web/src/main.tsx +10 -0
- package/web/src/pages/Dashboard.tsx +103 -0
- package/web/src/pages/Models.tsx +258 -0
- package/web/src/pages/Router.tsx +270 -0
- package/web/src/pages/RouterLogs.tsx +201 -0
- package/web/src/pages/ServerLogs.tsx +553 -0
- package/web/src/pages/Servers.tsx +358 -0
- package/web/src/types/api.ts +140 -0
- package/web/tailwind.config.js +31 -0
- package/web/tsconfig.app.json +28 -0
- package/web/tsconfig.json +7 -0
- package/web/tsconfig.node.json +26 -0
- package/web/vite.config.ts +25 -0
- package/MONITORING-ACCURACY-FIX.md +0 -199
- package/PER-PROCESS-METRICS.md +0 -190
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { modelDownloader, DownloadProgress } from './model-downloader';
|
|
3
|
+
import { stateManager } from './state-manager';
|
|
4
|
+
|
|
5
|
+
export type DownloadJobStatus = 'pending' | 'downloading' | 'completed' | 'failed' | 'cancelled';
|
|
6
|
+
|
|
7
|
+
export interface DownloadJob {
|
|
8
|
+
id: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
status: DownloadJobStatus;
|
|
12
|
+
progress: {
|
|
13
|
+
downloaded: number;
|
|
14
|
+
total: number;
|
|
15
|
+
percentage: number;
|
|
16
|
+
speed: string;
|
|
17
|
+
} | null;
|
|
18
|
+
error?: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
completedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface InternalJob extends DownloadJob {
|
|
24
|
+
abortController: AbortController;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Manages download jobs with progress tracking and cancellation support
|
|
29
|
+
*/
|
|
30
|
+
class DownloadJobManager {
|
|
31
|
+
private jobs: Map<string, InternalJob> = new Map();
|
|
32
|
+
private jobCounter = 0;
|
|
33
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
// Auto-cleanup completed/failed jobs after 5 minutes
|
|
37
|
+
this.cleanupInterval = setInterval(() => this.cleanupOldJobs(), 60000);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new download job
|
|
42
|
+
*/
|
|
43
|
+
createJob(repo: string, filename: string): string {
|
|
44
|
+
const id = `download-${Date.now()}-${++this.jobCounter}`;
|
|
45
|
+
const abortController = new AbortController();
|
|
46
|
+
|
|
47
|
+
const job: InternalJob = {
|
|
48
|
+
id,
|
|
49
|
+
repo,
|
|
50
|
+
filename,
|
|
51
|
+
status: 'pending',
|
|
52
|
+
progress: null,
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
abortController,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.jobs.set(id, job);
|
|
58
|
+
|
|
59
|
+
// Start download asynchronously
|
|
60
|
+
this.startDownload(job);
|
|
61
|
+
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a job by ID
|
|
67
|
+
*/
|
|
68
|
+
getJob(id: string): DownloadJob | null {
|
|
69
|
+
const job = this.jobs.get(id);
|
|
70
|
+
if (!job) return null;
|
|
71
|
+
|
|
72
|
+
// Return public job info (without abortController)
|
|
73
|
+
return this.toPublicJob(job);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List all jobs
|
|
78
|
+
*/
|
|
79
|
+
listJobs(): DownloadJob[] {
|
|
80
|
+
return Array.from(this.jobs.values()).map(job => this.toPublicJob(job));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cancel a download job
|
|
85
|
+
*/
|
|
86
|
+
cancelJob(id: string): boolean {
|
|
87
|
+
const job = this.jobs.get(id);
|
|
88
|
+
if (!job) return false;
|
|
89
|
+
|
|
90
|
+
if (job.status === 'pending' || job.status === 'downloading') {
|
|
91
|
+
job.abortController.abort();
|
|
92
|
+
job.status = 'cancelled';
|
|
93
|
+
job.completedAt = new Date().toISOString();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Delete a job from the list
|
|
102
|
+
*/
|
|
103
|
+
deleteJob(id: string): boolean {
|
|
104
|
+
const job = this.jobs.get(id);
|
|
105
|
+
if (!job) return false;
|
|
106
|
+
|
|
107
|
+
// Cancel if still running
|
|
108
|
+
if (job.status === 'pending' || job.status === 'downloading') {
|
|
109
|
+
job.abortController.abort();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.jobs.delete(id);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Start the download process for a job
|
|
118
|
+
*/
|
|
119
|
+
private async startDownload(job: InternalJob): Promise<void> {
|
|
120
|
+
job.status = 'downloading';
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const modelsDir = await stateManager.getModelsDirectory();
|
|
124
|
+
|
|
125
|
+
await modelDownloader.downloadModel(
|
|
126
|
+
job.repo,
|
|
127
|
+
job.filename,
|
|
128
|
+
(progress: DownloadProgress) => {
|
|
129
|
+
job.progress = {
|
|
130
|
+
downloaded: progress.downloaded,
|
|
131
|
+
total: progress.total,
|
|
132
|
+
percentage: progress.percentage,
|
|
133
|
+
speed: progress.speed,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
modelsDir,
|
|
137
|
+
{
|
|
138
|
+
silent: true,
|
|
139
|
+
signal: job.abortController.signal,
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Only mark as completed if not cancelled
|
|
144
|
+
if (job.status === 'downloading') {
|
|
145
|
+
job.status = 'completed';
|
|
146
|
+
job.completedAt = new Date().toISOString();
|
|
147
|
+
// Ensure progress shows 100%
|
|
148
|
+
if (job.progress) {
|
|
149
|
+
job.progress.percentage = 100;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Check if this was a cancellation (status may have been set by cancelJob)
|
|
154
|
+
const currentStatus = job.status as DownloadJobStatus;
|
|
155
|
+
if (currentStatus === 'cancelled') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const message = (error as Error).message;
|
|
160
|
+
if (message.includes('cancelled') || message.includes('interrupted')) {
|
|
161
|
+
job.status = 'cancelled';
|
|
162
|
+
} else {
|
|
163
|
+
job.status = 'failed';
|
|
164
|
+
job.error = message;
|
|
165
|
+
}
|
|
166
|
+
job.completedAt = new Date().toISOString();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert internal job to public job (strips internal fields)
|
|
172
|
+
*/
|
|
173
|
+
private toPublicJob(job: InternalJob): DownloadJob {
|
|
174
|
+
const { abortController, ...publicJob } = job;
|
|
175
|
+
return publicJob;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Clean up old completed/failed jobs
|
|
180
|
+
*/
|
|
181
|
+
private cleanupOldJobs(): void {
|
|
182
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
|
183
|
+
|
|
184
|
+
for (const [id, job] of this.jobs.entries()) {
|
|
185
|
+
if (
|
|
186
|
+
job.completedAt &&
|
|
187
|
+
new Date(job.completedAt).getTime() < fiveMinutesAgo
|
|
188
|
+
) {
|
|
189
|
+
this.jobs.delete(id);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Cleanup on shutdown
|
|
196
|
+
*/
|
|
197
|
+
shutdown(): void {
|
|
198
|
+
if (this.cleanupInterval) {
|
|
199
|
+
clearInterval(this.cleanupInterval);
|
|
200
|
+
this.cleanupInterval = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Cancel all active downloads
|
|
204
|
+
for (const job of this.jobs.values()) {
|
|
205
|
+
if (job.status === 'pending' || job.status === 'downloading') {
|
|
206
|
+
job.abortController.abort();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Export singleton instance
|
|
213
|
+
export const downloadJobManager = new DownloadJobManager();
|
|
@@ -41,7 +41,7 @@ export async function createMultiServerMonitorUI(
|
|
|
41
41
|
onModels?: (controls: MonitorUIControls) => void,
|
|
42
42
|
onFirstRender?: () => void
|
|
43
43
|
): Promise<MonitorUIControls> {
|
|
44
|
-
let updateInterval =
|
|
44
|
+
let updateInterval = 5000;
|
|
45
45
|
let intervalId: NodeJS.Timeout | null = null;
|
|
46
46
|
let viewMode: ViewMode = directJumpIndex !== undefined ? 'detail' : 'list';
|
|
47
47
|
let selectedServerIndex = directJumpIndex ?? 0;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type AdminStatus = 'running' | 'stopped' | 'crashed';
|
|
2
|
+
|
|
3
|
+
export interface AdminConfig {
|
|
4
|
+
id: 'admin';
|
|
5
|
+
port: number;
|
|
6
|
+
host: string;
|
|
7
|
+
apiKey: string; // Auto-generated on first start
|
|
8
|
+
|
|
9
|
+
// State tracking
|
|
10
|
+
status: AdminStatus;
|
|
11
|
+
pid?: number;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
lastStarted?: string;
|
|
14
|
+
lastStopped?: string;
|
|
15
|
+
|
|
16
|
+
// launchctl metadata
|
|
17
|
+
plistPath: string;
|
|
18
|
+
label: 'com.llama.admin';
|
|
19
|
+
stdoutPath: string;
|
|
20
|
+
stderrPath: string;
|
|
21
|
+
|
|
22
|
+
// Admin settings
|
|
23
|
+
requestTimeout: number; // ms for API requests (default: 30000)
|
|
24
|
+
verbose: boolean; // Enable verbose logging to file (default: false)
|
|
25
|
+
}
|
package/src/utils/log-parser.ts
CHANGED
|
@@ -18,6 +18,19 @@ export class LogParser {
|
|
|
18
18
|
private buffer: string[] = [];
|
|
19
19
|
private isBuffering = false;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Health check endpoints to filter out by default
|
|
23
|
+
* These are polled frequently by the TUI and generate excessive log noise
|
|
24
|
+
*/
|
|
25
|
+
private static readonly HEALTH_CHECK_ENDPOINTS = ['/health', '/slots', '/props'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a log line represents a health check request
|
|
29
|
+
*/
|
|
30
|
+
isHealthCheckRequest(line: string): boolean {
|
|
31
|
+
return LogParser.HEALTH_CHECK_ENDPOINTS.some(ep => line.includes(`GET ${ep} `));
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Check if line is a request status line (contains method/endpoint/status, no JSON)
|
|
23
36
|
* Handles both old and new formats:
|
package/web/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# llamacpp-cli Web UI
|
|
2
|
+
|
|
3
|
+
A React-based admin web interface for managing llama.cpp servers through the Admin REST API.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Web UI provides a clean, modern interface for managing your llama.cpp servers remotely. It's inspired by the Llama website design and built with:
|
|
8
|
+
|
|
9
|
+
- **React 19** - Modern React with concurrent features
|
|
10
|
+
- **Vite 7** - Fast build tooling and dev server
|
|
11
|
+
- **TypeScript 5.9** - Type-safe development
|
|
12
|
+
- **Tailwind CSS 4** - Utility-first styling with dark mode support
|
|
13
|
+
- **React Query** - Server state management with auto-refetch
|
|
14
|
+
- **React Router** - Client-side routing for SPA
|
|
15
|
+
- **Lucide React** - Beautiful icon library
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
### File Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
web/
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── components/
|
|
25
|
+
│ │ └── Nav.tsx # Navigation component with gradient logo
|
|
26
|
+
│ ├── pages/
|
|
27
|
+
│ │ ├── Dashboard.tsx # System overview and stats
|
|
28
|
+
│ │ ├── Servers.tsx # Server management (CRUD operations)
|
|
29
|
+
│ │ └── Models.tsx # Model management
|
|
30
|
+
│ ├── hooks/
|
|
31
|
+
│ │ └── useApi.ts # React Query hooks for all API operations
|
|
32
|
+
│ ├── lib/
|
|
33
|
+
│ │ └── api.ts # API client class with methods for all endpoints
|
|
34
|
+
│ ├── types/
|
|
35
|
+
│ │ └── api.ts # TypeScript types mirroring backend API
|
|
36
|
+
│ ├── App.tsx # Root component with routing
|
|
37
|
+
│ ├── main.tsx # Entry point with React Query provider
|
|
38
|
+
│ └── index.css # Global styles with dark theme
|
|
39
|
+
├── vite.config.ts # Vite configuration with API proxy
|
|
40
|
+
├── tailwind.config.js # Tailwind CSS configuration
|
|
41
|
+
├── postcss.config.js # PostCSS with Tailwind and Autoprefixer
|
|
42
|
+
├── package.json # Dependencies and scripts
|
|
43
|
+
└── README.md # This file
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### API Client
|
|
47
|
+
|
|
48
|
+
The API client (`src/lib/api.ts`) provides a full-featured client for the Admin API:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
class ApiClient {
|
|
52
|
+
// Server operations
|
|
53
|
+
async listServers(): Promise<{ servers: Server[] }>
|
|
54
|
+
async getServer(id: string): Promise<Server>
|
|
55
|
+
async createServer(data: CreateServerRequest): Promise<Server>
|
|
56
|
+
async updateServer(id: string, data: UpdateServerRequest): Promise<Server>
|
|
57
|
+
async deleteServer(id: string): Promise<void>
|
|
58
|
+
async startServer(id: string): Promise<Server>
|
|
59
|
+
async stopServer(id: string): Promise<Server>
|
|
60
|
+
async restartServer(id: string): Promise<Server>
|
|
61
|
+
|
|
62
|
+
// Model operations
|
|
63
|
+
async listModels(): Promise<{ models: Model[] }>
|
|
64
|
+
async getModel(name: string): Promise<Model>
|
|
65
|
+
async deleteModel(name: string, cascade?: boolean): Promise<void>
|
|
66
|
+
|
|
67
|
+
// System operations
|
|
68
|
+
async getHealth(): Promise<{ status: string }>
|
|
69
|
+
async getSystemStatus(): Promise<SystemStatus>
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Authentication:**
|
|
74
|
+
- API key stored in localStorage as `llama_admin_api_key`
|
|
75
|
+
- Sent as `Bearer` token in Authorization header
|
|
76
|
+
- Set via UI prompt on first load
|
|
77
|
+
|
|
78
|
+
### React Query Integration
|
|
79
|
+
|
|
80
|
+
All API operations are wrapped in React Query hooks (`src/hooks/useApi.ts`) for:
|
|
81
|
+
|
|
82
|
+
- **Auto-refetch:** Servers/status every 5s, models every 10s
|
|
83
|
+
- **Cache invalidation:** Mutations automatically invalidate relevant queries
|
|
84
|
+
- **Loading states:** Built-in loading/error states for all operations
|
|
85
|
+
- **Optimistic updates:** UI updates immediately on mutations
|
|
86
|
+
|
|
87
|
+
Example usage:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
function Servers() {
|
|
91
|
+
const { data, isLoading } = useServers();
|
|
92
|
+
const startServer = useStartServer();
|
|
93
|
+
|
|
94
|
+
const handleStart = async (id: string) => {
|
|
95
|
+
await startServer.mutateAsync(id);
|
|
96
|
+
// Query automatically refetches servers list
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Pages
|
|
102
|
+
|
|
103
|
+
#### Dashboard (`/dashboard`)
|
|
104
|
+
|
|
105
|
+
**Features:**
|
|
106
|
+
- 4 stat cards: Total Servers, Running, Stopped, Models
|
|
107
|
+
- Running servers list with details (port, threads, context)
|
|
108
|
+
- Auto-refresh every 5 seconds
|
|
109
|
+
- Clean gradient design
|
|
110
|
+
|
|
111
|
+
#### Servers (`/servers`)
|
|
112
|
+
|
|
113
|
+
**Features:**
|
|
114
|
+
- Table of all servers with status badges
|
|
115
|
+
- Per-server actions: Start/Stop/Restart/Delete
|
|
116
|
+
- Configuration display: threads, context size, GPU layers
|
|
117
|
+
- PID and uptime for running servers
|
|
118
|
+
- Confirmation dialogs for destructive actions
|
|
119
|
+
- Loading states for async operations
|
|
120
|
+
|
|
121
|
+
#### Models (`/models`)
|
|
122
|
+
|
|
123
|
+
**Features:**
|
|
124
|
+
- Table of all models with size and modified date
|
|
125
|
+
- Shows server usage count per model
|
|
126
|
+
- Delete with cascade option (also deletes associated servers)
|
|
127
|
+
- Formatted file sizes and dates
|
|
128
|
+
- Protection against deleting models in use
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
### Prerequisites
|
|
133
|
+
|
|
134
|
+
- Node.js 18+ (24.3.0 recommended)
|
|
135
|
+
- npm 9+ (11.4.2 recommended)
|
|
136
|
+
- Running Admin API server on `localhost:9200`
|
|
137
|
+
|
|
138
|
+
### Installation
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd web
|
|
142
|
+
npm install
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Development Server
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm run dev
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This starts Vite dev server on `http://localhost:5173` with:
|
|
152
|
+
- Hot Module Replacement (HMR)
|
|
153
|
+
- API proxy to `localhost:9200` for `/api` and `/health`
|
|
154
|
+
- Fast refresh for instant updates
|
|
155
|
+
|
|
156
|
+
### Vite Proxy Configuration
|
|
157
|
+
|
|
158
|
+
The dev server proxies API requests to avoid CORS issues:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// vite.config.ts
|
|
162
|
+
server: {
|
|
163
|
+
proxy: {
|
|
164
|
+
'/api': {
|
|
165
|
+
target: 'http://localhost:9200',
|
|
166
|
+
changeOrigin: true,
|
|
167
|
+
},
|
|
168
|
+
'/health': {
|
|
169
|
+
target: 'http://localhost:9200',
|
|
170
|
+
changeOrigin: true,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Production Build
|
|
177
|
+
|
|
178
|
+
### Build Static Assets
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm run build
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Output: `web/dist/` directory with:
|
|
185
|
+
- `index.html` - Entry point
|
|
186
|
+
- `assets/*.js` - Bundled JavaScript (code-split)
|
|
187
|
+
- `assets/*.css` - Bundled CSS
|
|
188
|
+
|
|
189
|
+
### Serving Static Files
|
|
190
|
+
|
|
191
|
+
The Admin API server automatically serves static files from `web/dist/`:
|
|
192
|
+
|
|
193
|
+
1. **Build the web UI:**
|
|
194
|
+
```bash
|
|
195
|
+
cd web
|
|
196
|
+
npm install
|
|
197
|
+
npm run build
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
2. **Start Admin API:**
|
|
201
|
+
```bash
|
|
202
|
+
llamacpp admin start
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
3. **Access UI:**
|
|
206
|
+
Open `http://localhost:9200` in your browser
|
|
207
|
+
|
|
208
|
+
**SPA Routing:**
|
|
209
|
+
- Non-API routes (`/dashboard`, `/servers`, `/models`) serve `index.html`
|
|
210
|
+
- API routes (`/api/*`) handled by REST API
|
|
211
|
+
- Static assets (`/assets/*`) served with long-term caching
|
|
212
|
+
|
|
213
|
+
**Error Handling:**
|
|
214
|
+
- If `web/dist` doesn't exist, returns helpful error:
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"error": "Not Found",
|
|
218
|
+
"details": "Static files not built. Run: cd web && npm install && npm run build"
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Environment Configuration
|
|
223
|
+
|
|
224
|
+
### API Endpoint
|
|
225
|
+
|
|
226
|
+
By default, the UI connects to the Admin API on the same host. To configure:
|
|
227
|
+
|
|
228
|
+
**Development:**
|
|
229
|
+
- Edit `vite.config.ts` proxy target
|
|
230
|
+
|
|
231
|
+
**Production:**
|
|
232
|
+
- API calls are relative (`/api/*`)
|
|
233
|
+
- Served from same origin as UI
|
|
234
|
+
|
|
235
|
+
### API Key
|
|
236
|
+
|
|
237
|
+
- Prompted on first load
|
|
238
|
+
- Stored in localStorage
|
|
239
|
+
- Can be cleared to re-prompt
|
|
240
|
+
|
|
241
|
+
## Styling
|
|
242
|
+
|
|
243
|
+
### Dark Theme
|
|
244
|
+
|
|
245
|
+
The UI uses a dark theme by default:
|
|
246
|
+
|
|
247
|
+
```css
|
|
248
|
+
:root {
|
|
249
|
+
color-scheme: dark;
|
|
250
|
+
background-color: #0a0a0a;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Tailwind CSS
|
|
255
|
+
|
|
256
|
+
Utility-first styling with responsive design:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
|
260
|
+
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white">
|
|
261
|
+
Title
|
|
262
|
+
</h1>
|
|
263
|
+
</div>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Component Patterns
|
|
267
|
+
|
|
268
|
+
- Consistent spacing: `px-6 py-8` for containers
|
|
269
|
+
- Borders: `border border-gray-200 dark:border-gray-800`
|
|
270
|
+
- Rounded corners: `rounded-lg` for cards
|
|
271
|
+
- Hover states: `hover:bg-gray-50 dark:hover:bg-gray-800/50`
|
|
272
|
+
- Status badges: Color-coded pills for server/model status
|
|
273
|
+
|
|
274
|
+
## API Integration
|
|
275
|
+
|
|
276
|
+
### Authentication
|
|
277
|
+
|
|
278
|
+
The UI handles API key authentication:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// On mount, check localStorage for API key
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
let key = localStorage.getItem('llama_admin_api_key');
|
|
284
|
+
if (!key) {
|
|
285
|
+
key = prompt('Enter Admin API Key:');
|
|
286
|
+
if (key) {
|
|
287
|
+
localStorage.setItem('llama_admin_api_key', key);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
api.setApiKey(key);
|
|
291
|
+
}, []);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Error Handling
|
|
295
|
+
|
|
296
|
+
All API calls handle errors gracefully:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
try {
|
|
300
|
+
await startServer.mutateAsync(id);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('Failed to start server:', error);
|
|
303
|
+
// React Query shows error state in UI
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Loading States
|
|
308
|
+
|
|
309
|
+
React Query provides loading states:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const { data, isLoading, error } = useServers();
|
|
313
|
+
|
|
314
|
+
if (isLoading) {
|
|
315
|
+
return <div>Loading...</div>;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (error) {
|
|
319
|
+
return <div>Error: {error.message}</div>;
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Testing
|
|
324
|
+
|
|
325
|
+
### Manual Testing Checklist
|
|
326
|
+
|
|
327
|
+
**Dashboard:**
|
|
328
|
+
- [ ] Stats display correctly
|
|
329
|
+
- [ ] Running servers list populates
|
|
330
|
+
- [ ] Auto-refresh updates data every 5s
|
|
331
|
+
|
|
332
|
+
**Servers:**
|
|
333
|
+
- [ ] Table shows all servers
|
|
334
|
+
- [ ] Start button works on stopped servers
|
|
335
|
+
- [ ] Stop button works on running servers
|
|
336
|
+
- [ ] Restart button works on running servers
|
|
337
|
+
- [ ] Delete shows confirmation dialog
|
|
338
|
+
- [ ] Status badges show correct colors
|
|
339
|
+
- [ ] PID and uptime display for running servers
|
|
340
|
+
|
|
341
|
+
**Models:**
|
|
342
|
+
- [ ] Table shows all models
|
|
343
|
+
- [ ] Sizes formatted correctly (GB/MB)
|
|
344
|
+
- [ ] Server usage count shows
|
|
345
|
+
- [ ] Delete asks for cascade confirmation
|
|
346
|
+
- [ ] Delete protected if servers use model
|
|
347
|
+
|
|
348
|
+
**Navigation:**
|
|
349
|
+
- [ ] Nav links work
|
|
350
|
+
- [ ] Active page highlighted
|
|
351
|
+
- [ ] Logo gradient displays
|
|
352
|
+
|
|
353
|
+
**Authentication:**
|
|
354
|
+
- [ ] API key prompt on first load
|
|
355
|
+
- [ ] API key persists in localStorage
|
|
356
|
+
- [ ] 401 errors handled
|
|
357
|
+
|
|
358
|
+
## Troubleshooting
|
|
359
|
+
|
|
360
|
+
### UI not loading
|
|
361
|
+
|
|
362
|
+
1. Check Admin API is running:
|
|
363
|
+
```bash
|
|
364
|
+
llamacpp admin status
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
2. Check static files built:
|
|
368
|
+
```bash
|
|
369
|
+
ls -la web/dist/
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
3. Rebuild if needed:
|
|
373
|
+
```bash
|
|
374
|
+
cd web && npm run build
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### API calls failing
|
|
378
|
+
|
|
379
|
+
1. Check API key is correct:
|
|
380
|
+
```bash
|
|
381
|
+
llamacpp admin status # Shows current API key
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
2. Clear localStorage and re-enter key:
|
|
385
|
+
```javascript
|
|
386
|
+
localStorage.removeItem('llama_admin_api_key');
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
3. Check browser console for errors
|
|
390
|
+
|
|
391
|
+
### Development server not starting
|
|
392
|
+
|
|
393
|
+
1. Check port 5173 is available
|
|
394
|
+
2. Check node_modules installed:
|
|
395
|
+
```bash
|
|
396
|
+
ls -la node_modules/
|
|
397
|
+
```
|
|
398
|
+
3. Try reinstalling:
|
|
399
|
+
```bash
|
|
400
|
+
rm -rf node_modules package-lock.json
|
|
401
|
+
npm install
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Future Enhancements
|
|
405
|
+
|
|
406
|
+
Potential features for future versions:
|
|
407
|
+
|
|
408
|
+
- [ ] Server configuration editing from UI
|
|
409
|
+
- [ ] Model search and download from HuggingFace
|
|
410
|
+
- [ ] Real-time logs viewer (WebSocket)
|
|
411
|
+
- [ ] Performance graphs (CPU, memory, GPU over time)
|
|
412
|
+
- [ ] Dark/light theme toggle
|
|
413
|
+
- [ ] Server templates for quick creation
|
|
414
|
+
- [ ] Bulk operations (start/stop multiple servers)
|
|
415
|
+
- [ ] User preferences (polling interval, theme, etc.)
|
|
416
|
+
|
|
417
|
+
## Contributing
|
|
418
|
+
|
|
419
|
+
When adding new features:
|
|
420
|
+
|
|
421
|
+
1. Update types in `src/types/api.ts`
|
|
422
|
+
2. Add API methods to `src/lib/api.ts`
|
|
423
|
+
3. Add React Query hooks to `src/hooks/useApi.ts`
|
|
424
|
+
4. Create/update page components in `src/pages/`
|
|
425
|
+
5. Update this README
|
|
426
|
+
|
|
427
|
+
## License
|
|
428
|
+
|
|
429
|
+
Same license as llamacpp-cli project.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|