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.
Files changed (83) hide show
  1. package/README.md +51 -29
  2. package/build/shared/src/pulsemcp-admin-client/lib/cleanup-good-jobs.js +34 -0
  3. package/build/shared/src/pulsemcp-admin-client/lib/discard-good-job.js +24 -0
  4. package/build/shared/src/pulsemcp-admin-client/lib/force-trigger-good-job-cron.js +24 -0
  5. package/build/shared/src/pulsemcp-admin-client/lib/get-good-job-cron-schedules.js +28 -0
  6. package/build/shared/src/pulsemcp-admin-client/lib/get-good-job-processes.js +34 -0
  7. package/build/shared/src/pulsemcp-admin-client/lib/get-good-job-statistics.js +29 -0
  8. package/build/shared/src/pulsemcp-admin-client/lib/get-good-job.js +36 -0
  9. package/build/shared/src/pulsemcp-admin-client/lib/get-good-jobs.js +66 -0
  10. package/build/shared/src/pulsemcp-admin-client/lib/reschedule-good-job.js +30 -0
  11. package/build/shared/src/pulsemcp-admin-client/lib/retry-good-job.js +24 -0
  12. package/build/shared/src/pulsemcp-admin-client/lib/run-exam-for-mirror.js +49 -0
  13. package/build/shared/src/pulsemcp-admin-client/lib/save-results-for-mirror.js +34 -0
  14. package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +108 -0
  15. package/build/shared/src/server.js +50 -0
  16. package/build/shared/src/tools/cleanup-good-jobs.js +70 -0
  17. package/build/shared/src/tools/discard-good-job.js +48 -0
  18. package/build/shared/src/tools/force-trigger-good-job-cron.js +51 -0
  19. package/build/shared/src/tools/get-good-job-queue-statistics.js +52 -0
  20. package/build/shared/src/tools/get-good-job.js +80 -0
  21. package/build/shared/src/tools/list-good-job-cron-schedules.js +59 -0
  22. package/build/shared/src/tools/list-good-job-processes.js +59 -0
  23. package/build/shared/src/tools/list-good-jobs.js +107 -0
  24. package/build/shared/src/tools/reschedule-good-job.js +58 -0
  25. package/build/shared/src/tools/retry-good-job.js +47 -0
  26. package/build/shared/src/tools/run-exam-for-mirror.js +113 -0
  27. package/build/shared/src/tools/save-results-for-mirror.js +99 -0
  28. package/build/shared/src/tools.js +192 -58
  29. package/package.json +1 -1
  30. package/shared/pulsemcp-admin-client/lib/cleanup-good-jobs.d.ts +6 -0
  31. package/shared/pulsemcp-admin-client/lib/cleanup-good-jobs.js +34 -0
  32. package/shared/pulsemcp-admin-client/lib/discard-good-job.d.ts +3 -0
  33. package/shared/pulsemcp-admin-client/lib/discard-good-job.js +24 -0
  34. package/shared/pulsemcp-admin-client/lib/force-trigger-good-job-cron.d.ts +3 -0
  35. package/shared/pulsemcp-admin-client/lib/force-trigger-good-job-cron.js +24 -0
  36. package/shared/pulsemcp-admin-client/lib/get-good-job-cron-schedules.d.ts +3 -0
  37. package/shared/pulsemcp-admin-client/lib/get-good-job-cron-schedules.js +28 -0
  38. package/shared/pulsemcp-admin-client/lib/get-good-job-processes.d.ts +3 -0
  39. package/shared/pulsemcp-admin-client/lib/get-good-job-processes.js +34 -0
  40. package/shared/pulsemcp-admin-client/lib/get-good-job-statistics.d.ts +3 -0
  41. package/shared/pulsemcp-admin-client/lib/get-good-job-statistics.js +29 -0
  42. package/shared/pulsemcp-admin-client/lib/get-good-job.d.ts +3 -0
  43. package/shared/pulsemcp-admin-client/lib/get-good-job.js +36 -0
  44. package/shared/pulsemcp-admin-client/lib/get-good-jobs.d.ts +11 -0
  45. package/shared/pulsemcp-admin-client/lib/get-good-jobs.js +66 -0
  46. package/shared/pulsemcp-admin-client/lib/reschedule-good-job.d.ts +3 -0
  47. package/shared/pulsemcp-admin-client/lib/reschedule-good-job.js +30 -0
  48. package/shared/pulsemcp-admin-client/lib/retry-good-job.d.ts +3 -0
  49. package/shared/pulsemcp-admin-client/lib/retry-good-job.js +24 -0
  50. package/shared/pulsemcp-admin-client/lib/run-exam-for-mirror.d.ts +3 -0
  51. package/shared/pulsemcp-admin-client/lib/run-exam-for-mirror.js +49 -0
  52. package/shared/pulsemcp-admin-client/lib/save-results-for-mirror.d.ts +3 -0
  53. package/shared/pulsemcp-admin-client/lib/save-results-for-mirror.js +34 -0
  54. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +108 -0
  55. package/shared/server.d.ts +47 -1
  56. package/shared/server.js +50 -0
  57. package/shared/tools/cleanup-good-jobs.d.ts +35 -0
  58. package/shared/tools/cleanup-good-jobs.js +70 -0
  59. package/shared/tools/discard-good-job.d.ts +30 -0
  60. package/shared/tools/discard-good-job.js +48 -0
  61. package/shared/tools/force-trigger-good-job-cron.d.ts +30 -0
  62. package/shared/tools/force-trigger-good-job-cron.js +51 -0
  63. package/shared/tools/get-good-job-queue-statistics.d.ts +24 -0
  64. package/shared/tools/get-good-job-queue-statistics.js +52 -0
  65. package/shared/tools/get-good-job.d.ts +30 -0
  66. package/shared/tools/get-good-job.js +80 -0
  67. package/shared/tools/list-good-job-cron-schedules.d.ts +24 -0
  68. package/shared/tools/list-good-job-cron-schedules.js +59 -0
  69. package/shared/tools/list-good-job-processes.d.ts +24 -0
  70. package/shared/tools/list-good-job-processes.js +59 -0
  71. package/shared/tools/list-good-jobs.d.ts +57 -0
  72. package/shared/tools/list-good-jobs.js +107 -0
  73. package/shared/tools/reschedule-good-job.d.ts +34 -0
  74. package/shared/tools/reschedule-good-job.js +58 -0
  75. package/shared/tools/retry-good-job.d.ts +30 -0
  76. package/shared/tools/retry-good-job.js +47 -0
  77. package/shared/tools/run-exam-for-mirror.d.ts +49 -0
  78. package/shared/tools/run-exam-for-mirror.js +113 -0
  79. package/shared/tools/save-results-for-mirror.d.ts +58 -0
  80. package/shared/tools/save-results-for-mirror.js +99 -0
  81. package/shared/tools.d.ts +18 -4
  82. package/shared/tools.js +192 -58
  83. 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` | 5 | Full MCP server directory (read + write) |
100
- | `server_directory_readonly` | 3 | MCP server directory read-only |
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
+ }