@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.
Files changed (105) 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/lib/admin-manager.d.ts +111 -0
  34. package/dist/lib/admin-manager.d.ts.map +1 -0
  35. package/dist/lib/admin-manager.js +413 -0
  36. package/dist/lib/admin-manager.js.map +1 -0
  37. package/dist/lib/admin-server.d.ts +148 -0
  38. package/dist/lib/admin-server.d.ts.map +1 -0
  39. package/dist/lib/admin-server.js +1161 -0
  40. package/dist/lib/admin-server.js.map +1 -0
  41. package/dist/lib/download-job-manager.d.ts +64 -0
  42. package/dist/lib/download-job-manager.d.ts.map +1 -0
  43. package/dist/lib/download-job-manager.js +164 -0
  44. package/dist/lib/download-job-manager.js.map +1 -0
  45. package/dist/tui/MultiServerMonitorApp.js +1 -1
  46. package/dist/types/admin-config.d.ts +19 -0
  47. package/dist/types/admin-config.d.ts.map +1 -0
  48. package/dist/types/admin-config.js +3 -0
  49. package/dist/types/admin-config.js.map +1 -0
  50. package/dist/utils/log-parser.d.ts +9 -0
  51. package/dist/utils/log-parser.d.ts.map +1 -1
  52. package/dist/utils/log-parser.js +11 -0
  53. package/dist/utils/log-parser.js.map +1 -1
  54. package/docs/images/web-ui-servers.png +0 -0
  55. package/package.json +1 -1
  56. package/src/cli.ts +100 -0
  57. package/src/commands/admin/config.ts +121 -0
  58. package/src/commands/admin/logs.ts +91 -0
  59. package/src/commands/admin/restart.ts +26 -0
  60. package/src/commands/admin/start.ts +27 -0
  61. package/src/commands/admin/status.ts +84 -0
  62. package/src/commands/admin/stop.ts +16 -0
  63. package/src/commands/logs.ts +24 -0
  64. package/src/lib/admin-manager.ts +435 -0
  65. package/src/lib/admin-server.ts +1243 -0
  66. package/src/lib/download-job-manager.ts +213 -0
  67. package/src/tui/MultiServerMonitorApp.ts +1 -1
  68. package/src/types/admin-config.ts +25 -0
  69. package/src/utils/log-parser.ts +13 -0
  70. package/web/README.md +429 -0
  71. package/web/eslint.config.js +23 -0
  72. package/web/index.html +13 -0
  73. package/web/llamacpp-web-dist.tar.gz +0 -0
  74. package/web/package-lock.json +4017 -0
  75. package/web/package.json +38 -0
  76. package/web/postcss.config.js +6 -0
  77. package/web/public/vite.svg +1 -0
  78. package/web/src/App.css +42 -0
  79. package/web/src/App.tsx +86 -0
  80. package/web/src/assets/react.svg +1 -0
  81. package/web/src/components/ApiKeyPrompt.tsx +71 -0
  82. package/web/src/components/CreateServerModal.tsx +372 -0
  83. package/web/src/components/DownloadProgress.tsx +123 -0
  84. package/web/src/components/Nav.tsx +89 -0
  85. package/web/src/components/RouterConfigModal.tsx +240 -0
  86. package/web/src/components/SearchModal.tsx +306 -0
  87. package/web/src/components/ServerConfigModal.tsx +291 -0
  88. package/web/src/hooks/useApi.ts +259 -0
  89. package/web/src/index.css +42 -0
  90. package/web/src/lib/api.ts +226 -0
  91. package/web/src/main.tsx +10 -0
  92. package/web/src/pages/Dashboard.tsx +103 -0
  93. package/web/src/pages/Models.tsx +258 -0
  94. package/web/src/pages/Router.tsx +270 -0
  95. package/web/src/pages/RouterLogs.tsx +201 -0
  96. package/web/src/pages/ServerLogs.tsx +553 -0
  97. package/web/src/pages/Servers.tsx +358 -0
  98. package/web/src/types/api.ts +140 -0
  99. package/web/tailwind.config.js +31 -0
  100. package/web/tsconfig.app.json +28 -0
  101. package/web/tsconfig.json +7 -0
  102. package/web/tsconfig.node.json +26 -0
  103. package/web/vite.config.ts +25 -0
  104. package/MONITORING-ACCURACY-FIX.md +0 -199
  105. 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 = 2000;
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
+ }
@@ -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
+ ])