pulsemcp-cms-admin-mcp-server 0.6.8 → 0.6.10
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/README.md +51 -29
- package/build/shared/src/pulsemcp-admin-client/lib/cleanup-good-jobs.js +34 -0
- package/build/shared/src/pulsemcp-admin-client/lib/discard-good-job.js +24 -0
- package/build/shared/src/pulsemcp-admin-client/lib/force-trigger-good-job-cron.js +24 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-good-job-cron-schedules.js +28 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-good-job-processes.js +34 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-good-job-statistics.js +29 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-good-job.js +36 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-good-jobs.js +66 -0
- package/build/shared/src/pulsemcp-admin-client/lib/reschedule-good-job.js +30 -0
- package/build/shared/src/pulsemcp-admin-client/lib/retry-good-job.js +24 -0
- package/build/shared/src/pulsemcp-admin-client/lib/run-exam-for-mirror.js +49 -0
- package/build/shared/src/pulsemcp-admin-client/lib/save-results-for-mirror.js +34 -0
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +108 -0
- package/build/shared/src/server.js +50 -0
- package/build/shared/src/tools/cleanup-good-jobs.js +70 -0
- package/build/shared/src/tools/discard-good-job.js +48 -0
- package/build/shared/src/tools/force-trigger-good-job-cron.js +51 -0
- package/build/shared/src/tools/get-good-job-queue-statistics.js +52 -0
- package/build/shared/src/tools/get-good-job.js +80 -0
- package/build/shared/src/tools/list-good-job-cron-schedules.js +59 -0
- package/build/shared/src/tools/list-good-job-processes.js +59 -0
- package/build/shared/src/tools/list-good-jobs.js +107 -0
- package/build/shared/src/tools/reschedule-good-job.js +58 -0
- package/build/shared/src/tools/retry-good-job.js +47 -0
- package/build/shared/src/tools/run-exam-for-mirror.js +113 -0
- package/build/shared/src/tools/save-results-for-mirror.js +99 -0
- package/build/shared/src/tools.js +192 -58
- package/package.json +1 -1
- package/shared/pulsemcp-admin-client/lib/cleanup-good-jobs.d.ts +6 -0
- package/shared/pulsemcp-admin-client/lib/cleanup-good-jobs.js +34 -0
- package/shared/pulsemcp-admin-client/lib/discard-good-job.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/discard-good-job.js +24 -0
- package/shared/pulsemcp-admin-client/lib/force-trigger-good-job-cron.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/force-trigger-good-job-cron.js +24 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job-cron-schedules.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job-cron-schedules.js +28 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job-processes.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job-processes.js +34 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job-statistics.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job-statistics.js +29 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-good-job.js +36 -0
- package/shared/pulsemcp-admin-client/lib/get-good-jobs.d.ts +11 -0
- package/shared/pulsemcp-admin-client/lib/get-good-jobs.js +66 -0
- package/shared/pulsemcp-admin-client/lib/reschedule-good-job.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/reschedule-good-job.js +30 -0
- package/shared/pulsemcp-admin-client/lib/retry-good-job.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/retry-good-job.js +24 -0
- package/shared/pulsemcp-admin-client/lib/run-exam-for-mirror.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/run-exam-for-mirror.js +49 -0
- package/shared/pulsemcp-admin-client/lib/save-results-for-mirror.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/save-results-for-mirror.js +34 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +108 -0
- package/shared/server.d.ts +47 -1
- package/shared/server.js +50 -0
- package/shared/tools/cleanup-good-jobs.d.ts +35 -0
- package/shared/tools/cleanup-good-jobs.js +70 -0
- package/shared/tools/discard-good-job.d.ts +30 -0
- package/shared/tools/discard-good-job.js +48 -0
- package/shared/tools/force-trigger-good-job-cron.d.ts +30 -0
- package/shared/tools/force-trigger-good-job-cron.js +51 -0
- package/shared/tools/get-good-job-queue-statistics.d.ts +24 -0
- package/shared/tools/get-good-job-queue-statistics.js +52 -0
- package/shared/tools/get-good-job.d.ts +30 -0
- package/shared/tools/get-good-job.js +80 -0
- package/shared/tools/list-good-job-cron-schedules.d.ts +24 -0
- package/shared/tools/list-good-job-cron-schedules.js +59 -0
- package/shared/tools/list-good-job-processes.d.ts +24 -0
- package/shared/tools/list-good-job-processes.js +59 -0
- package/shared/tools/list-good-jobs.d.ts +57 -0
- package/shared/tools/list-good-jobs.js +107 -0
- package/shared/tools/reschedule-good-job.d.ts +34 -0
- package/shared/tools/reschedule-good-job.js +58 -0
- package/shared/tools/retry-good-job.d.ts +30 -0
- package/shared/tools/retry-good-job.js +47 -0
- package/shared/tools/run-exam-for-mirror.d.ts +49 -0
- package/shared/tools/run-exam-for-mirror.js +113 -0
- package/shared/tools/save-results-for-mirror.d.ts +58 -0
- package/shared/tools/save-results-for-mirror.js +99 -0
- package/shared/tools.d.ts +18 -4
- package/shared/tools.js +192 -58
- package/shared/types.d.ts +92 -0
package/README.md
CHANGED
|
@@ -82,6 +82,18 @@ This server is built and tested on macOS with Claude Desktop. It should work wit
|
|
|
82
82
|
| `create_redirect` | redirects | write | Create a new URL redirect entry. |
|
|
83
83
|
| `update_redirect` | redirects | write | Update an existing URL redirect by ID. |
|
|
84
84
|
| `delete_redirect` | redirects | write | Delete a URL redirect by ID (irreversible). |
|
|
85
|
+
| `list_good_jobs` | good_jobs | read | List and filter background jobs by queue, status, job class, and date range. |
|
|
86
|
+
| `get_good_job` | good_jobs | read | Get detailed information about a specific background job. |
|
|
87
|
+
| `list_good_job_cron_schedules` | good_jobs | read | List all configured cron schedules. |
|
|
88
|
+
| `list_good_job_processes` | good_jobs | read | List active worker processes. |
|
|
89
|
+
| `get_good_job_queue_statistics` | good_jobs | read | Get aggregate job statistics by status. |
|
|
90
|
+
| `retry_good_job` | good_jobs | write | Retry a failed or discarded background job. |
|
|
91
|
+
| `discard_good_job` | good_jobs | write | Discard a background job to prevent retries. |
|
|
92
|
+
| `reschedule_good_job` | good_jobs | write | Reschedule a background job to a new time. |
|
|
93
|
+
| `force_trigger_good_job_cron` | good_jobs | write | Force trigger a cron schedule immediately. |
|
|
94
|
+
| `cleanup_good_jobs` | good_jobs | write | Clean up old background jobs by status and age. |
|
|
95
|
+
| `run_exam_for_mirror` | proctor | write | Run proctor exams against unofficial mirrors via Fly Machines. |
|
|
96
|
+
| `save_results_for_mirror` | proctor | write | Save proctor exam results (must use output from `run_exam_for_mirror`). |
|
|
85
97
|
|
|
86
98
|
# Tool Groups
|
|
87
99
|
|
|
@@ -92,35 +104,38 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
92
104
|
|
|
93
105
|
## Available Groups
|
|
94
106
|
|
|
95
|
-
| Group | Tools | Description
|
|
96
|
-
| ----------------------------- | ----- |
|
|
97
|
-
| `newsletter` | 6 | Full newsletter management (read + write)
|
|
98
|
-
| `newsletter_readonly` | 3 | Newsletter read-only (get posts, authors)
|
|
99
|
-
| `server_directory` |
|
|
100
|
-
| `server_directory_readonly` |
|
|
101
|
-
| `official_queue` | 7 | Full official mirror queue (read + write)
|
|
102
|
-
| `official_queue_readonly` | 2 | Official mirror queue read-only
|
|
103
|
-
| `unofficial_mirrors` | 5 | Full unofficial mirrors CRUD (read + write)
|
|
104
|
-
| `unofficial_mirrors_readonly` | 2 | Unofficial mirrors read-only
|
|
105
|
-
| `official_mirrors` | 2 | Official mirrors REST API (read-only)
|
|
106
|
-
| `official_mirrors_readonly` | 2 | Official mirrors read-only (alias)
|
|
107
|
-
| `tenants` | 2 | Tenants REST API (read-only)
|
|
108
|
-
| `tenants_readonly` | 2 | Tenants read-only (alias)
|
|
109
|
-
| `mcp_jsons` | 5 | Full MCP JSON configurations (read + write)
|
|
110
|
-
| `mcp_jsons_readonly` | 2 | MCP JSON configurations read-only
|
|
111
|
-
| `mcp_servers` | 3 | Full MCP servers management (read + write)
|
|
112
|
-
| `mcp_servers_readonly` | 2 | MCP servers read-only (list, get)
|
|
113
|
-
| `redirects` | 5 | Full URL redirect management (read + write)
|
|
114
|
-
| `redirects_readonly` | 2 | URL redirects read-only (list, get)
|
|
107
|
+
| Group | Tools | Description |
|
|
108
|
+
| ----------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `newsletter` | 6 | Full newsletter management (read + write) |
|
|
110
|
+
| `newsletter_readonly` | 3 | Newsletter read-only (get posts, authors) |
|
|
111
|
+
| `server_directory` | 27 | Comprehensive superset: includes all tools from mcp_servers, unofficial_mirrors, official_mirrors, official_queue, mcp_jsons, plus implementations/providers (read + write) |
|
|
112
|
+
| `server_directory_readonly` | 13 | Server directory read-only subset |
|
|
113
|
+
| `official_queue` | 7 | Full official mirror queue (read + write) |
|
|
114
|
+
| `official_queue_readonly` | 2 | Official mirror queue read-only |
|
|
115
|
+
| `unofficial_mirrors` | 5 | Full unofficial mirrors CRUD (read + write) |
|
|
116
|
+
| `unofficial_mirrors_readonly` | 2 | Unofficial mirrors read-only |
|
|
117
|
+
| `official_mirrors` | 2 | Official mirrors REST API (read-only) |
|
|
118
|
+
| `official_mirrors_readonly` | 2 | Official mirrors read-only (alias) |
|
|
119
|
+
| `tenants` | 2 | Tenants REST API (read-only) |
|
|
120
|
+
| `tenants_readonly` | 2 | Tenants read-only (alias) |
|
|
121
|
+
| `mcp_jsons` | 5 | Full MCP JSON configurations (read + write) |
|
|
122
|
+
| `mcp_jsons_readonly` | 2 | MCP JSON configurations read-only |
|
|
123
|
+
| `mcp_servers` | 3 | Full MCP servers management (read + write) |
|
|
124
|
+
| `mcp_servers_readonly` | 2 | MCP servers read-only (list, get) |
|
|
125
|
+
| `redirects` | 5 | Full URL redirect management (read + write) |
|
|
126
|
+
| `redirects_readonly` | 2 | URL redirects read-only (list, get) |
|
|
127
|
+
| `good_jobs` | 10 | Full GoodJob background job management (read + write) |
|
|
128
|
+
| `good_jobs_readonly` | 5 | GoodJob read-only (list, get, stats, processes, cron) |
|
|
129
|
+
| `proctor` | 2 | Proctor exam execution and result storage (write-only, no readonly variant) |
|
|
115
130
|
|
|
116
131
|
### Tools by Group
|
|
117
132
|
|
|
118
133
|
- **newsletter** / **newsletter_readonly**:
|
|
119
134
|
- Read-only: `get_newsletter_posts`, `get_newsletter_post`, `get_authors`
|
|
120
135
|
- Write: `draft_newsletter_post`, `update_newsletter_post`, `upload_image`
|
|
121
|
-
- **server_directory** / **server_directory_readonly
|
|
122
|
-
- Read-only: `search_mcp_implementations`, `get_draft_mcp_implementations`, `find_providers`
|
|
123
|
-
- Write: `save_mcp_implementation`, `send_impl_posted_notif`
|
|
136
|
+
- **server_directory** / **server_directory_readonly** (superset — includes tools from mcp_servers, unofficial_mirrors, official_mirrors, official_queue, and mcp_jsons):
|
|
137
|
+
- Read-only: `search_mcp_implementations`, `get_draft_mcp_implementations`, `find_providers`, `list_mcp_servers`, `get_mcp_server`, `get_unofficial_mirrors`, `get_unofficial_mirror`, `get_official_mirrors`, `get_official_mirror`, `get_official_mirror_queue_items`, `get_official_mirror_queue_item`, `get_mcp_jsons`, `get_mcp_json`
|
|
138
|
+
- Write: `save_mcp_implementation`, `send_impl_posted_notif`, `update_mcp_server`, `create_unofficial_mirror`, `update_unofficial_mirror`, `delete_unofficial_mirror`, `approve_official_mirror_queue_item`, `approve_mirror_no_modify`, `reject_official_mirror_queue_item`, `add_official_mirror_to_regular_queue`, `unlink_official_mirror_queue_item`, `create_mcp_json`, `update_mcp_json`, `delete_mcp_json`
|
|
124
139
|
- **official_queue** / **official_queue_readonly**:
|
|
125
140
|
- Read-only: `get_official_mirror_queue_items`, `get_official_mirror_queue_item`
|
|
126
141
|
- Write: `approve_official_mirror_queue_item`, `approve_mirror_no_modify`, `reject_official_mirror_queue_item`, `add_official_mirror_to_regular_queue`, `unlink_official_mirror_queue_item`
|
|
@@ -140,12 +155,17 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
140
155
|
- **redirects** / **redirects_readonly**:
|
|
141
156
|
- Read-only: `get_redirects`, `get_redirect`
|
|
142
157
|
- Write: `create_redirect`, `update_redirect`, `delete_redirect`
|
|
158
|
+
- **good_jobs** / **good_jobs_readonly**:
|
|
159
|
+
- Read-only: `list_good_jobs`, `get_good_job`, `list_good_job_cron_schedules`, `list_good_job_processes`, `get_good_job_queue_statistics`
|
|
160
|
+
- Write: `retry_good_job`, `discard_good_job`, `reschedule_good_job`, `force_trigger_good_job_cron`, `cleanup_good_jobs`
|
|
161
|
+
- **proctor** (no readonly variant — both tools trigger side effects):
|
|
162
|
+
- Write: `run_exam_for_mirror`, `save_results_for_mirror`
|
|
143
163
|
|
|
144
164
|
## Environment Variables
|
|
145
165
|
|
|
146
|
-
| Variable | Description | Default
|
|
147
|
-
| ------------- | ------------------------------------------- |
|
|
148
|
-
| `TOOL_GROUPS` | Comma-separated list of enabled tool groups | `newsletter,server_directory,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers,redirects` (all base groups) |
|
|
166
|
+
| Variable | Description | Default |
|
|
167
|
+
| ------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
168
|
+
| `TOOL_GROUPS` | Comma-separated list of enabled tool groups | `newsletter,server_directory,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers,redirects,good_jobs,proctor` (all base groups) |
|
|
149
169
|
|
|
150
170
|
## Examples
|
|
151
171
|
|
|
@@ -170,9 +190,11 @@ TOOL_GROUPS=server_directory_readonly
|
|
|
170
190
|
Enable all groups with read-only access:
|
|
171
191
|
|
|
172
192
|
```bash
|
|
173
|
-
TOOL_GROUPS=newsletter_readonly,server_directory_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly,redirects_readonly
|
|
193
|
+
TOOL_GROUPS=newsletter_readonly,server_directory_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly,redirects_readonly,good_jobs_readonly
|
|
174
194
|
```
|
|
175
195
|
|
|
196
|
+
Note: `proctor` has no readonly variant since both tools trigger side effects.
|
|
197
|
+
|
|
176
198
|
Mix full and read-only access per group:
|
|
177
199
|
|
|
178
200
|
```bash
|
|
@@ -306,7 +328,7 @@ Add to your Claude Desktop configuration:
|
|
|
306
328
|
"args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
|
|
307
329
|
"env": {
|
|
308
330
|
"PULSEMCP_ADMIN_API_KEY": "your-api-key-here",
|
|
309
|
-
"TOOL_GROUPS": "newsletter,server_directory,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers,redirects"
|
|
331
|
+
"TOOL_GROUPS": "newsletter,server_directory,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers,redirects,good_jobs,proctor"
|
|
310
332
|
}
|
|
311
333
|
}
|
|
312
334
|
}
|
|
@@ -323,7 +345,7 @@ For read-only access:
|
|
|
323
345
|
"args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
|
|
324
346
|
"env": {
|
|
325
347
|
"PULSEMCP_ADMIN_API_KEY": "your-api-key-here",
|
|
326
|
-
"TOOL_GROUPS": "newsletter_readonly,server_directory_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly,redirects_readonly"
|
|
348
|
+
"TOOL_GROUPS": "newsletter_readonly,server_directory_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly,redirects_readonly,good_jobs_readonly"
|
|
327
349
|
}
|
|
328
350
|
}
|
|
329
351
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export async function cleanupGoodJobs(apiKey, baseUrl, params) {
|
|
2
|
+
const url = new URL('/api/good_jobs/cleanup', baseUrl);
|
|
3
|
+
const body = {};
|
|
4
|
+
if (params?.older_than_days !== undefined) {
|
|
5
|
+
body.older_than_days = params.older_than_days;
|
|
6
|
+
}
|
|
7
|
+
if (params?.status !== undefined) {
|
|
8
|
+
body.status = params.status;
|
|
9
|
+
}
|
|
10
|
+
const response = await fetch(url.toString(), {
|
|
11
|
+
method: 'DELETE',
|
|
12
|
+
headers: {
|
|
13
|
+
'X-API-Key': apiKey,
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify(body),
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
if (response.status === 401) {
|
|
21
|
+
throw new Error('Invalid API key');
|
|
22
|
+
}
|
|
23
|
+
if (response.status === 403) {
|
|
24
|
+
throw new Error('User lacks write privileges');
|
|
25
|
+
}
|
|
26
|
+
if (response.status === 422) {
|
|
27
|
+
const errorData = (await response.json());
|
|
28
|
+
throw new Error(`Validation failed: ${errorData.errors?.join(', ') || 'Unknown error'}`);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Failed to cleanup good jobs: ${response.status} ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const data = (await response.json());
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function discardGoodJob(apiKey, baseUrl, id) {
|
|
2
|
+
const url = new URL(`/api/good_jobs/${id}/discard`, baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'POST',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
if (response.status === 401) {
|
|
12
|
+
throw new Error('Invalid API key');
|
|
13
|
+
}
|
|
14
|
+
if (response.status === 403) {
|
|
15
|
+
throw new Error('User lacks write privileges');
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 404) {
|
|
18
|
+
throw new Error(`GoodJob with ID ${id} not found`);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to discard good job: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
const data = (await response.json());
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function forceTriggerGoodJobCron(apiKey, baseUrl, cronKey) {
|
|
2
|
+
const url = new URL(`/api/good_jobs/cron_schedules/${cronKey}/trigger`, baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'POST',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
if (response.status === 401) {
|
|
12
|
+
throw new Error('Invalid API key');
|
|
13
|
+
}
|
|
14
|
+
if (response.status === 403) {
|
|
15
|
+
throw new Error('User lacks write privileges');
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 404) {
|
|
18
|
+
throw new Error(`Cron schedule with key "${cronKey}" not found`);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to trigger cron schedule: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
const data = (await response.json());
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function getGoodJobCronSchedules(apiKey, baseUrl) {
|
|
2
|
+
const url = new URL('/api/good_jobs/cron_schedules', baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'GET',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
if (response.status === 401) {
|
|
12
|
+
throw new Error('Invalid API key');
|
|
13
|
+
}
|
|
14
|
+
if (response.status === 403) {
|
|
15
|
+
throw new Error('User lacks admin privileges');
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Failed to fetch cron schedules: ${response.status} ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
const data = (await response.json());
|
|
20
|
+
return data.map((schedule) => ({
|
|
21
|
+
cron_key: schedule.cron_key,
|
|
22
|
+
job_class: schedule.job_class,
|
|
23
|
+
cron_expression: schedule.cron_expression,
|
|
24
|
+
description: schedule.description,
|
|
25
|
+
next_scheduled_at: schedule.next_scheduled_at,
|
|
26
|
+
last_run_at: schedule.last_run_at,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export async function getGoodJobProcesses(apiKey, baseUrl) {
|
|
2
|
+
const url = new URL('/api/good_jobs/processes', baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'GET',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
if (response.status === 401) {
|
|
12
|
+
throw new Error('Invalid API key');
|
|
13
|
+
}
|
|
14
|
+
if (response.status === 403) {
|
|
15
|
+
throw new Error('User lacks admin privileges');
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Failed to fetch good job processes: ${response.status} ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
const json = (await response.json());
|
|
20
|
+
const processes = json.data;
|
|
21
|
+
return processes.map((proc) => {
|
|
22
|
+
const schedulers = proc.state.schedulers ?? [];
|
|
23
|
+
const queues = schedulers.map((s) => s.queues);
|
|
24
|
+
const maxThreads = schedulers.reduce((sum, s) => sum + s.max_threads, 0);
|
|
25
|
+
return {
|
|
26
|
+
id: proc.id,
|
|
27
|
+
hostname: proc.state.hostname,
|
|
28
|
+
pid: proc.state.pid,
|
|
29
|
+
queues,
|
|
30
|
+
max_threads: maxThreads || undefined,
|
|
31
|
+
started_at: proc.created_at,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export async function getGoodJobStatistics(apiKey, baseUrl) {
|
|
2
|
+
const url = new URL('/api/good_jobs/statistics', baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'GET',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
if (response.status === 401) {
|
|
12
|
+
throw new Error('Invalid API key');
|
|
13
|
+
}
|
|
14
|
+
if (response.status === 403) {
|
|
15
|
+
throw new Error('User lacks admin privileges');
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Failed to fetch job statistics: ${response.status} ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
const data = (await response.json());
|
|
20
|
+
return {
|
|
21
|
+
total: data.total,
|
|
22
|
+
scheduled: data.by_status.scheduled,
|
|
23
|
+
queued: data.by_status.queued,
|
|
24
|
+
running: data.by_status.running,
|
|
25
|
+
succeeded: data.by_status.succeeded,
|
|
26
|
+
failed: data.by_status.retried ?? 0,
|
|
27
|
+
discarded: data.by_status.discarded,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export async function getGoodJob(apiKey, baseUrl, id) {
|
|
2
|
+
const url = new URL(`/api/good_jobs/${id}`, baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'GET',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
if (response.status === 401) {
|
|
12
|
+
throw new Error('Invalid API key');
|
|
13
|
+
}
|
|
14
|
+
if (response.status === 403) {
|
|
15
|
+
throw new Error('User lacks admin privileges');
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 404) {
|
|
18
|
+
throw new Error(`GoodJob with ID ${id} not found`);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to fetch good job: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
const job = (await response.json());
|
|
23
|
+
return {
|
|
24
|
+
id: job.id,
|
|
25
|
+
job_class: job.job_class,
|
|
26
|
+
queue_name: job.queue_name,
|
|
27
|
+
status: job.status,
|
|
28
|
+
scheduled_at: job.scheduled_at,
|
|
29
|
+
performed_at: job.performed_at,
|
|
30
|
+
finished_at: job.finished_at,
|
|
31
|
+
error: job.error,
|
|
32
|
+
serialized_params: job.serialized_params,
|
|
33
|
+
created_at: job.created_at,
|
|
34
|
+
updated_at: job.updated_at,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function mapGoodJob(job) {
|
|
2
|
+
return {
|
|
3
|
+
id: job.id,
|
|
4
|
+
job_class: job.job_class,
|
|
5
|
+
queue_name: job.queue_name,
|
|
6
|
+
status: job.status,
|
|
7
|
+
scheduled_at: job.scheduled_at,
|
|
8
|
+
performed_at: job.performed_at,
|
|
9
|
+
finished_at: job.finished_at,
|
|
10
|
+
error: job.error,
|
|
11
|
+
serialized_params: job.serialized_params,
|
|
12
|
+
created_at: job.created_at,
|
|
13
|
+
updated_at: job.updated_at,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export async function getGoodJobs(apiKey, baseUrl, params) {
|
|
17
|
+
const url = new URL('/api/good_jobs', baseUrl);
|
|
18
|
+
if (params?.queue_name) {
|
|
19
|
+
url.searchParams.append('queue_name', params.queue_name);
|
|
20
|
+
}
|
|
21
|
+
if (params?.status) {
|
|
22
|
+
url.searchParams.append('status', params.status);
|
|
23
|
+
}
|
|
24
|
+
if (params?.job_class) {
|
|
25
|
+
url.searchParams.append('job_class', params.job_class);
|
|
26
|
+
}
|
|
27
|
+
if (params?.after) {
|
|
28
|
+
url.searchParams.append('after', params.after);
|
|
29
|
+
}
|
|
30
|
+
if (params?.before) {
|
|
31
|
+
url.searchParams.append('before', params.before);
|
|
32
|
+
}
|
|
33
|
+
if (params?.limit) {
|
|
34
|
+
url.searchParams.append('limit', params.limit.toString());
|
|
35
|
+
}
|
|
36
|
+
if (params?.offset) {
|
|
37
|
+
url.searchParams.append('offset', params.offset.toString());
|
|
38
|
+
}
|
|
39
|
+
const response = await fetch(url.toString(), {
|
|
40
|
+
method: 'GET',
|
|
41
|
+
headers: {
|
|
42
|
+
'X-API-Key': apiKey,
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
if (response.status === 401) {
|
|
48
|
+
throw new Error('Invalid API key');
|
|
49
|
+
}
|
|
50
|
+
if (response.status === 403) {
|
|
51
|
+
throw new Error('User lacks admin privileges');
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Failed to fetch good jobs: ${response.status} ${response.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
const data = (await response.json());
|
|
56
|
+
return {
|
|
57
|
+
jobs: data.data.map(mapGoodJob),
|
|
58
|
+
pagination: {
|
|
59
|
+
current_page: data.meta.current_page,
|
|
60
|
+
total_pages: data.meta.total_pages,
|
|
61
|
+
total_count: data.meta.total_count,
|
|
62
|
+
has_next: data.meta.has_next,
|
|
63
|
+
limit: data.meta.limit,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export async function rescheduleGoodJob(apiKey, baseUrl, id, scheduledAt) {
|
|
2
|
+
const url = new URL(`/api/good_jobs/${id}/reschedule`, baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'POST',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
Accept: 'application/json',
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({ scheduled_at: scheduledAt }),
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
if (response.status === 401) {
|
|
14
|
+
throw new Error('Invalid API key');
|
|
15
|
+
}
|
|
16
|
+
if (response.status === 403) {
|
|
17
|
+
throw new Error('User lacks write privileges');
|
|
18
|
+
}
|
|
19
|
+
if (response.status === 404) {
|
|
20
|
+
throw new Error(`GoodJob with ID ${id} not found`);
|
|
21
|
+
}
|
|
22
|
+
if (response.status === 422) {
|
|
23
|
+
const errorData = (await response.json());
|
|
24
|
+
throw new Error(`Validation failed: ${errorData.errors?.join(', ') || 'Unknown error'}`);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Failed to reschedule good job: ${response.status} ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
const data = (await response.json());
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function retryGoodJob(apiKey, baseUrl, id) {
|
|
2
|
+
const url = new URL(`/api/good_jobs/${id}/retry`, baseUrl);
|
|
3
|
+
const response = await fetch(url.toString(), {
|
|
4
|
+
method: 'POST',
|
|
5
|
+
headers: {
|
|
6
|
+
'X-API-Key': apiKey,
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
if (response.status === 401) {
|
|
12
|
+
throw new Error('Invalid API key');
|
|
13
|
+
}
|
|
14
|
+
if (response.status === 403) {
|
|
15
|
+
throw new Error('User lacks write privileges');
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 404) {
|
|
18
|
+
throw new Error(`GoodJob with ID ${id} not found`);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to retry good job: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
const data = (await response.json());
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export async function runExamForMirror(apiKey, baseUrl, params) {
|
|
2
|
+
const url = new URL('/api/proctor/run_exam_for_mirror', baseUrl);
|
|
3
|
+
const body = {
|
|
4
|
+
mirror_ids: params.mirror_ids,
|
|
5
|
+
runtime_id: params.runtime_id,
|
|
6
|
+
exam_type: params.exam_type,
|
|
7
|
+
};
|
|
8
|
+
if (params.max_retries !== undefined) {
|
|
9
|
+
body.max_retries = params.max_retries;
|
|
10
|
+
}
|
|
11
|
+
const response = await fetch(url.toString(), {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'X-API-Key': apiKey,
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
Accept: 'application/x-ndjson',
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
if (response.status === 401) {
|
|
22
|
+
throw new Error('Invalid API key');
|
|
23
|
+
}
|
|
24
|
+
if (response.status === 403) {
|
|
25
|
+
throw new Error('User lacks admin privileges');
|
|
26
|
+
}
|
|
27
|
+
if (response.status === 422) {
|
|
28
|
+
const errorData = (await response.json());
|
|
29
|
+
throw new Error(`Validation failed: ${errorData.error || 'Unknown error'}`);
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Failed to run proctor exam: ${response.status} ${response.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
// Parse NDJSON response - each line is a separate JSON object
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
const lines = [];
|
|
36
|
+
for (const line of text.split('\n')) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (trimmed) {
|
|
39
|
+
try {
|
|
40
|
+
lines.push(JSON.parse(trimmed));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Include malformed lines as error entries so they're visible in output
|
|
44
|
+
lines.push({ type: 'error', message: `Malformed NDJSON line: ${trimmed}` });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { lines };
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export async function saveResultsForMirror(apiKey, baseUrl, params) {
|
|
2
|
+
const url = new URL('/api/proctor/save_results_for_mirror', baseUrl);
|
|
3
|
+
const body = {
|
|
4
|
+
mirror_id: params.mirror_id,
|
|
5
|
+
runtime_id: params.runtime_id,
|
|
6
|
+
results: params.results,
|
|
7
|
+
};
|
|
8
|
+
const response = await fetch(url.toString(), {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'X-API-Key': apiKey,
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
Accept: 'application/json',
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify(body),
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
if (response.status === 401) {
|
|
19
|
+
throw new Error('Invalid API key');
|
|
20
|
+
}
|
|
21
|
+
if (response.status === 403) {
|
|
22
|
+
throw new Error('User lacks admin privileges');
|
|
23
|
+
}
|
|
24
|
+
if (response.status === 404) {
|
|
25
|
+
throw new Error(`Mirror not found with ID: ${params.mirror_id}`);
|
|
26
|
+
}
|
|
27
|
+
if (response.status === 422) {
|
|
28
|
+
const errorData = (await response.json());
|
|
29
|
+
throw new Error(`Validation failed: ${errorData.error || 'Unknown error'}`);
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Failed to save proctor results: ${response.status} ${response.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
return (await response.json());
|
|
34
|
+
}
|