@aion0/forge 0.10.20 → 0.10.23

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.
@@ -0,0 +1,617 @@
1
+ # TP `/automation` Page — API Reference
2
+
3
+ Reference for the HTTP endpoints used by TP's Automation page
4
+ (`/automation`, source: `frontend/src/pages/Automation/Automation.jsx`)
5
+ and the related upgrade / testbed workflows the page invokes through
6
+ shared infrastructure.
7
+
8
+ All endpoints are mounted under the `adc` Django app:
9
+
10
+ ```
11
+ <TP-base-url>/adc/<endpoint>
12
+ ```
13
+
14
+ `<TP-base-url>` examples:
15
+ - Production: `https://nac-tp.fortinet-us.com`
16
+ - Test: `http://10.15.33.25:8000`
17
+ - Dev (.11): `http://10.15.33.11:8000`
18
+
19
+ ## Authentication
20
+
21
+ Every endpoint requires a JWT in the `Authorization` header:
22
+
23
+ ```
24
+ Authorization: JWT <token>
25
+ ```
26
+
27
+ Mint a token:
28
+
29
+ ```bash
30
+ T=$(curl -s -X POST <TP-base-url>/token-auth/ \
31
+ -H 'Content-Type: application/json' \
32
+ -d '{"username":"<user>","password":"<pw>"}' | jq -r .token)
33
+ ```
34
+
35
+ In the examples below, `$T` stands for the JWT. Tokens are
36
+ short-lived; on 401 the frontend redirects to SSO, and scripts should
37
+ re-mint.
38
+
39
+ ---
40
+
41
+ ## Endpoints called by the `/automation` page
42
+
43
+ ### `GET /adc/automation-verion/`
44
+
45
+ Returns the FortiNAC versions the automation pipeline tracks.
46
+ Populates the version dropdown.
47
+
48
+ Response:
49
+ ```json
50
+ {"versions": ["7.4.6", "7.4.7", "7.6.5", "7.6.6"]}
51
+ ```
52
+
53
+ Backed by: `adc.views.dashboard.dashboardviews.get_automation_version`
54
+
55
+ ### `GET /adc/get_testcases`
56
+
57
+ Walks the cloned test-framework repo on TP, parses every Python test
58
+ file, and returns a hierarchical tree of modules → test cases.
59
+
60
+ **Side-effect:** pulls latest commits on the `main` branch of the local
61
+ clone before parsing. Calling from a script will rebase TP's local
62
+ repo on `main` — fine in normal use, but worth knowing if a developer
63
+ is hand-testing branches on the TP host.
64
+
65
+ Response shape (truncated):
66
+ ```json
67
+ {
68
+ "tests": {
69
+ "L2": {
70
+ "test_l2_radius.py": [
71
+ "test_basic_auth",
72
+ "test_radius_attributes"
73
+ ]
74
+ },
75
+ "L3": {}
76
+ }
77
+ }
78
+ ```
79
+
80
+ Backed by: `automationview.get_testcases`
81
+
82
+ ### `POST /adc/pytest_run`
83
+
84
+ Kicks off a pytest execution on the chosen automation testbed. Creates
85
+ a `PytestExecution` row and returns its id; the actual run is
86
+ asynchronous.
87
+
88
+ Body:
89
+ ```json
90
+ {
91
+ "user": "alice",
92
+ "lab": "L2Mode_7",
93
+ "testcase": [
94
+ "tests/L2/test_l2_radius.py::test_basic_auth",
95
+ "tests/L2/test_l2_radius.py::test_radius_attributes"
96
+ ],
97
+ "argument": "-k 'radius and not flaky' --tb=short -vv"
98
+ }
99
+ ```
100
+
101
+ | Field | Type | Notes |
102
+ |---|---|---|
103
+ | `user` | string | TP username of the caller. |
104
+ | `lab` | string | AT lab name from `AutomationTBUser` (the same names returned by `get_automation_lab`). Comma-separate multiple labs (`"L2Mode_7,L2Mode_9"`). The caller must already own or be a member of `usedby` on the lab, and no other execution can be `Running`/`Initiating` against it. |
105
+ | `testcase` | **list of strings** | Each entry is a pytest test-id path. The handler iterates the list, prefixes each with the test-framework repo path on TP, and joins them with spaces before invoking pytest. |
106
+ | `argument` | string | **Raw pytest CLI arguments**, injected verbatim between the testcase paths and the framework's `--html=...`/`--rack-file ...` flags. Use this for filters, verbosity, fail-fast, collect-only, marker expressions, etc. |
107
+
108
+ The handler also accepts any **extra** fields in the body — they're
109
+ preserved on the execution record. You can attach tracking metadata
110
+ (`mantis_id`, `jenkins_job`, etc.) without backend changes.
111
+
112
+ #### What gets executed on the testbed
113
+
114
+ The worker generates a shell script of the form:
115
+
116
+ ```bash
117
+ git pull origin main
118
+ cd <repo>
119
+ source venv/bin/activate
120
+ export PYTHONPATH=<test-framework>:<tests>
121
+ export DISPLAY=:99
122
+ pytest <testcase paths> <argument> --collect-only
123
+ pytest <testcase paths> <argument> --html=<report> --rack-file <rack> <pytest_options>
124
+ ```
125
+
126
+ So `argument` is literally the slot for any pytest flag. The worker
127
+ runs `--collect-only` first as a dry-run check, then the real run.
128
+
129
+ #### Examples
130
+
131
+ **Run one test:**
132
+ ```json
133
+ {
134
+ "user": "alice",
135
+ "lab": "L2Mode_7",
136
+ "testcase": ["tests/L2/test_l2_radius.py::test_basic_auth"],
137
+ "argument": ""
138
+ }
139
+ ```
140
+
141
+ **Run several specific tests, with verbose output and fail-fast:**
142
+ ```json
143
+ {
144
+ "user": "alice",
145
+ "lab": "L2Mode_7",
146
+ "testcase": [
147
+ "tests/L2/test_l2_radius.py::test_basic_auth",
148
+ "tests/L3/test_l3_vpn.py::test_vpn_login"
149
+ ],
150
+ "argument": "-vv -x"
151
+ }
152
+ ```
153
+
154
+ **Run every test in a file, filtered by keyword:**
155
+ ```json
156
+ {
157
+ "user": "alice",
158
+ "lab": "L2Mode_7",
159
+ "testcase": ["tests/L2/test_l2_radius.py"],
160
+ "argument": "-k 'radius and not flaky'"
161
+ }
162
+ ```
163
+
164
+ **Dry-run / list-only (no test bodies executed by the second pytest invocation):**
165
+ ```json
166
+ {
167
+ "user": "alice",
168
+ "lab": "L2Mode_7",
169
+ "testcase": ["tests/L2/"],
170
+ "argument": "--collect-only"
171
+ }
172
+ ```
173
+
174
+ (Note: `--collect-only` runs twice in this case — once by the worker's
175
+ hardcoded first-line check, once by your explicit flag. Functionally
176
+ identical to a normal collect-only.)
177
+
178
+ #### Response
179
+
180
+ ```json
181
+ {"exec_id": "53"}
182
+ ```
183
+ Or on error:
184
+ ```json
185
+ {"error": "..."}
186
+ ```
187
+
188
+ On lab conflict (someone else's run is in progress on that lab), the
189
+ response contains `conflict_labs` and `usable_labs` instead of
190
+ `exec_id`.
191
+
192
+ Backed by: `automationview.pytest_run` → `PytestAction.create_execution_entry`
193
+
194
+ ### `POST /adc/get_automation_lab`
195
+
196
+ Lists the automation testbeds the calling user can see (owned or
197
+ shared). Each entry includes a live `status` field computed from
198
+ in-progress executions.
199
+
200
+ Body:
201
+ ```json
202
+ {"user": "alice"}
203
+ ```
204
+
205
+ Response (truncated):
206
+ ```json
207
+ [
208
+ {
209
+ "name": "L2Mode_7",
210
+ "usedby": "alice,bob",
211
+ "status": "Available"
212
+ }
213
+ ]
214
+ ```
215
+
216
+ `status` is `"Running"` when any `PytestExecution` for this lab has
217
+ status `Running`, else `"Available"`.
218
+
219
+ ### `POST /adc/get_test_result`
220
+
221
+ Returns the calling user's recent test executions with computed
222
+ progress percentages.
223
+
224
+ Body:
225
+ ```json
226
+ {"user": "alice"}
227
+ ```
228
+
229
+ Response:
230
+ ```json
231
+ [
232
+ {
233
+ "id": 53,
234
+ "name": "test001",
235
+ "report_file_path": "media/automation/53/report.html",
236
+ "log_file_path": "media/automation/53/log.txt",
237
+ "status": "Running",
238
+ "progress": 42
239
+ }
240
+ ]
241
+ ```
242
+
243
+ `progress = (pass + fail + skip + error) / total * 100`, rounded.
244
+
245
+ `report_file_path` and `log_file_path` are server-relative; open them
246
+ by prepending `<TP-base-url>` and sending the JWT.
247
+
248
+ ### `POST /adc/get_test_execution_by_id`
249
+
250
+ Returns one execution's full record (steps, counts, environment,
251
+ timestamps) for the detail panel.
252
+
253
+ Body:
254
+ ```json
255
+ {"id": 53}
256
+ ```
257
+
258
+ Response: serialized `PytestExecution`. Returns 404 if not found.
259
+
260
+ ### `POST /adc/test_exec_kill`
261
+
262
+ Aborts a running execution. The row stays in the DB with a terminal
263
+ status; the underlying pytest subprocess is sent SIGTERM.
264
+
265
+ Body:
266
+ ```json
267
+ {"id": 53}
268
+ ```
269
+
270
+ ### `POST /adc/test_exec_delete`
271
+
272
+ Deletes one or many `PytestExecution` rows and their on-disk
273
+ artifacts. Accepts either a single id or a list.
274
+
275
+ Body (single):
276
+ ```json
277
+ {"id": 53}
278
+ ```
279
+
280
+ Body (bulk):
281
+ ```json
282
+ {"historyresults": [50, 51, 52]}
283
+ ```
284
+
285
+ ### `POST /adc/automation_lock`
286
+
287
+ Marks a testbed as in-use, or releases it. Used to serialize tests
288
+ that need exclusive access to shared hardware.
289
+
290
+ Body:
291
+ ```json
292
+ {"action": "lock", "user": "alice", "name": "L2Mode_7"}
293
+ ```
294
+
295
+ `action` is `"lock"` or `"unlock"`.
296
+
297
+ ### `POST /adc/automation_tb_set_viewers/`
298
+
299
+ Edits who can *see* a testbed (separate from ownership/usage).
300
+
301
+ Body:
302
+ ```json
303
+ {
304
+ "action": "add",
305
+ "tb_name": "L2Mode_7",
306
+ "viewers": ["bob", "carol"]
307
+ }
308
+ ```
309
+
310
+ `action` is `"add"`, `"remove"`, or `"set"`.
311
+
312
+ ### `GET /adc/status-check/`
313
+
314
+ Generic shared-lab status payload used by the status widget rendered
315
+ on the page. Not specific to automation but called from it.
316
+
317
+ Backed by: `adc.views.sharedlab.tbviews.ClassicCombinedStatus`
318
+
319
+ ### `GET /user/get_user/<username>`
320
+
321
+ Returns the calling user's profile (role, group, default project) so
322
+ the page can decide what controls to render. Note: outside the `adc`
323
+ namespace.
324
+
325
+ ---
326
+
327
+ ## NAC upgrade APIs
328
+
329
+ The `/automation` page does not call these directly, but every
330
+ automation run that targets a specific build relies on the testbed
331
+ having been upgraded first. There are **two modes** of upgrade:
332
+
333
+ | Mode | What it targets | Endpoints |
334
+ |---|---|---|
335
+ | **Device mode** | One specific `ip` | `nac-upgrade/`, `nac-upgrade-version-snapshot/`, `nac-upgrade-snapshot/`, `check-upgrade-status/` |
336
+ | **Testbed mode** | Every NAC/NCM device in a testbed (discovered from `deployinfo`) | `nac-upgrade-testbed/` |
337
+
338
+ Both modes share the same `upgrade_type` axis:
339
+
340
+ | `upgrade_type` | Required extra field | Meaning |
341
+ |---|---|---|
342
+ | `GA` | (none) | Pull the latest GA image from the build server |
343
+ | `build` | `build_number` | Download a specific build (e.g. `7.6.6.0123`) and install it |
344
+ | `file` | `file_uploaded` (multipart) | Install from an uploaded image |
345
+ | `command` | `command` | Run a raw upgrade CLI command (advanced/manual) |
346
+
347
+ Upgrade work is **asynchronous** in both modes — the call returns once
348
+ the job is queued. Use the device-mode status endpoint to poll.
349
+
350
+ ### Device mode
351
+
352
+ #### `POST /adc/nac-upgrade/`
353
+
354
+ Upgrade a single FortiNAC device. Accepts `multipart/form-data` so it
355
+ can carry the uploaded image file.
356
+
357
+ Form fields:
358
+ - `ip` — target device IP (required)
359
+ - `upgrade_type` — `GA` / `build` / `file` / `command`
360
+ - `build_number` — when `upgrade_type=build`
361
+ - `file_uploaded` — when `upgrade_type=file` (image file as multipart)
362
+ - `command` — when `upgrade_type=command`
363
+
364
+ Response:
365
+ ```json
366
+ {"status": "success", "message": "..."}
367
+ ```
368
+ HTTP `202` on success, `400` on failure.
369
+
370
+ Backed by: `nacviews.nac_upgrade`
371
+
372
+ #### `POST /adc/nac-upgrade-version-snapshot/`
373
+
374
+ Take a pre-upgrade snapshot of the device's current image so it can be
375
+ reverted later. Returns once the snapshot job has been queued.
376
+
377
+ Body:
378
+ ```json
379
+ {
380
+ "ip": "10.15.40.42",
381
+ "dev_name": "fortinac01",
382
+ "build_number": "7.6.6.0123",
383
+ "upgrade_type": "build",
384
+ "major_version": "7.6",
385
+ "minor_version": "6",
386
+ "rp_prefix": "rp",
387
+ "command": "<optional, when upgrade_type=command>"
388
+ }
389
+ ```
390
+
391
+ Backed by: `nacupgradeview.nac_upgrade_version_snapshot`
392
+
393
+ #### `POST /adc/nac-upgrade-snapshot/`
394
+
395
+ Upgrade *with* automatic snapshot/revert. Same payload as
396
+ `nac-upgrade-version-snapshot/` plus:
397
+
398
+ - `revert_snapshot` — `true` / `false`; when `true`, the device is
399
+ reverted to the snapshot if the upgrade fails.
400
+
401
+ Backed by: `nacupgradeview.nac_upgrade_snapshot`
402
+
403
+ #### `POST /adc/check-upgrade-status/`
404
+
405
+ Poll the upgrade status of a single device.
406
+
407
+ Body:
408
+ ```json
409
+ {"ip": "10.15.40.42"}
410
+ ```
411
+
412
+ Response: state of the most recent upgrade task for that IP. Shape:
413
+ ```json
414
+ {
415
+ "is_upgrading": false,
416
+ "last_task_status": "SUCCESS",
417
+ "last_task_type": "build",
418
+ "last_build_number": "7.6.6.0123",
419
+ "last_image_name": "FortiNAC-F-7.6.6.0123.out",
420
+ "last_major_version": "7.6",
421
+ "last_minor_version": "6",
422
+ "updated_at": "2026-05-28T11:42:01.123456+00:00",
423
+ "log": "...full log tail...",
424
+ "ip": "10.15.40.42"
425
+ }
426
+ ```
427
+
428
+ `is_upgrading` is `true` when `last_task_status` is `PENDING` or
429
+ `PROGRESS`. The endpoint works for both modes — see the table at the
430
+ top of the section for which testbed-mode `upgrade_type`s actually
431
+ write to this table — but read the polling caveats below carefully.
432
+
433
+ ##### Polling caveat — testbed-mode `build`
434
+
435
+ `run_download_async` (the worker that handles `upgrade_type=build` in
436
+ testbed mode) does **not** set `last_task_status` to `PENDING` or
437
+ `PROGRESS` while running. It only writes `SUCCESS` on completion.
438
+ Effect: while a testbed-mode build upgrade is in flight,
439
+ `is_upgrading` returns `false` even though the worker is actively
440
+ downloading + restoring on the device.
441
+
442
+ To get live progress mid-run for testbed-mode build upgrades, read
443
+ the `log` field instead — `run_download_async` appends to it
444
+ continuously (download start, restore start, VM CLI output, success).
445
+
446
+ > **Known bug (worth fixing as a separate task):** `run_download_async`
447
+ > in `backend/adc/views/sharedlab/nacviews.py` should set
448
+ > `last_task_status = "PROGRESS"` immediately after `get_or_create`
449
+ > at the start of the worker, and `"FAILURE"` in its `except` branch
450
+ > (it currently only writes `"SUCCESS"` on the happy path). With that
451
+ > fix, `check-upgrade-status/` would return an accurate `is_upgrading`
452
+ > flag for testbed-mode build upgrades and surface failures without
453
+ > requiring the caller to parse the log field.
454
+
455
+ ##### Polling caveat — testbed-mode `GA`
456
+
457
+ `upgrade_type=GA` in `nac_upgrade_testbed` is a stub — it returns
458
+ `{"upgrade_type": "GA"}` immediately and never touches `SingleDevice`.
459
+ This endpoint will report `"Device not found"` (404) for IPs that have
460
+ never been upgraded by another mode. There is no work to poll.
461
+
462
+ ##### Polling caveat — testbed-mode `file` and `command`
463
+
464
+ These branches run synchronously inside the HTTP request and only
465
+ write to `SingleDevice` *after* the work finishes. The request itself
466
+ blocks until then (file restore can take minutes), so polling
467
+ `check-upgrade-status/` for in-flight progress isn't useful for these
468
+ types — by the time you can issue a separate poll, the upgrade is
469
+ already done.
470
+
471
+ Backed by: `nacupgradeview.check_upgrade_status`
472
+
473
+ ### Testbed mode
474
+
475
+ #### `POST /adc/nac-upgrade-testbed/`
476
+
477
+ Upgrade every NAC/NCM device in a testbed at once. Accepts
478
+ `multipart/form-data` to carry an optional uploaded image.
479
+
480
+ The handler reads `deployinfo` (JSON), finds every key matching
481
+ `dev<N>` whose value contains `nac` or `ncm`, and pairs it with the
482
+ corresponding `ip<N>` to build the upgrade list — then runs the
483
+ chosen `upgrade_type` against each in parallel.
484
+
485
+ Form fields:
486
+ - `deployinfo` — JSON string with the testbed's device map. Shape:
487
+ `{"dev1":"fortinac01","ip1":"10.15.40.42","dev2":"ncm01","ip2":"10.15.40.43", ...}`
488
+ (same as `AutomationTBSerializer.deployinfo`)
489
+ - `upgrade_type` — `GA` / `build` / `file` / `command`
490
+ - `build_number` — when `upgrade_type=build`
491
+ - `file_uploaded` — when `upgrade_type=file`
492
+ - `command` — when `upgrade_type=command`
493
+
494
+ Response:
495
+ ```json
496
+ {"upgrade_type": "build"}
497
+ ```
498
+
499
+ To monitor progress, poll `check-upgrade-status/` per-IP for each NAC
500
+ in the testbed.
501
+
502
+ Backed by: `nacviews.nac_upgrade_testbed`
503
+
504
+ ### Diagnostic
505
+
506
+ #### `POST /adc/test_thread_db/`
507
+
508
+ Diagnostic helper that exercises threaded DB writes against the
509
+ `SingleDevice` model — used to verify upgrade-worker threading on a
510
+ host. Not used by the UI; do not call in production.
511
+
512
+ Backed by: `nacupgradeview.test_thread_db`
513
+
514
+ ---
515
+
516
+ ## Other related APIs the `/automation` workflow touches
517
+
518
+ These belong to other pages (Shared Lab, Automation Testbed) but are
519
+ part of the same domain. Listed here so the dev team can find them
520
+ without spelunking.
521
+
522
+ ### Automation testbed management — `adc.views.automation.automation_tbview`
523
+
524
+ | Endpoint | Method | Purpose |
525
+ |---|---|---|
526
+ | `/adc/create_automation_testbed/` | POST | Create a new automation testbed entry |
527
+ | `/adc/automationtb-list/` | POST | List testbeds by `category` + `keyword` filter |
528
+ | `/adc/automationtb-moduletitle/` | GET | Distinct module titles across testbeds (dropdown source) |
529
+ | `/adc/automationtb-update/` | POST | Update a testbed's metadata, `deployinfo`, or `new_name` |
530
+
531
+ ### Performance lab (parallel of automation, used by `/performance`)
532
+
533
+ Mirror endpoints exist for performance-test workflows; they share the
534
+ same shape as the automation ones above. Listed for completeness in
535
+ case the dev team needs to script performance runs the same way.
536
+
537
+ | Endpoint | Method | Mirrors |
538
+ |---|---|---|
539
+ | `/adc/get_perf_testcases/` | GET | `get_testcases` |
540
+ | `/adc/get_perf_testcases_module/` | GET | (module-level filter) |
541
+ | `/adc/perf_robot_run/` | POST | `pytest_run` (Robot Framework instead of pytest) |
542
+ | `/adc/get_perf_lab/` | POST | `get_automation_lab` |
543
+ | `/adc/get_perf_test_result/` | POST | `get_test_result` |
544
+ | `/adc/get_perf_test_execution_by_id/` | POST | `get_test_execution_by_id` |
545
+ | `/adc/performance_lock/` | POST | `automation_lock` |
546
+ | `/adc/perf_exec_kill/` | POST | `test_exec_kill` |
547
+ | `/adc/perf_exec_delete/` | POST | `test_exec_delete` |
548
+
549
+ ---
550
+
551
+ ## Quick smoke test from the command line
552
+
553
+ ```bash
554
+ T=$(curl -s -X POST http://10.15.33.11:8000/token-auth/ \
555
+ -H 'Content-Type: application/json' \
556
+ -d '{"username":"admin","password":"<your-pw>"}' | jq -r .token)
557
+
558
+ # 1. Pull versions
559
+ curl -s -H "Authorization: JWT $T" \
560
+ http://10.15.33.11:8000/adc/automation-verion/ | jq .
561
+
562
+ # 2. List your testbeds
563
+ curl -s -X POST -H "Authorization: JWT $T" \
564
+ -H 'Content-Type: application/json' \
565
+ -d '{"user":"admin"}' \
566
+ http://10.15.33.11:8000/adc/get_automation_lab | jq '.[] | {name, status}'
567
+
568
+ # 3. Fire a pytest run
569
+ EXEC_ID=$(curl -s -X POST -H "Authorization: JWT $T" \
570
+ -H 'Content-Type: application/json' \
571
+ -d '{
572
+ "user":"admin",
573
+ "lab":"L2Mode_7",
574
+ "testcase":["tests/L2/test_l2_radius.py::test_basic_auth"],
575
+ "argument":"-vv"
576
+ }' \
577
+ http://10.15.33.11:8000/adc/pytest_run | jq -r .exec_id)
578
+ echo "Started exec $EXEC_ID"
579
+
580
+ # 4. Poll progress
581
+ curl -s -X POST -H "Authorization: JWT $T" \
582
+ -H 'Content-Type: application/json' \
583
+ -d "{\"id\":$EXEC_ID}" \
584
+ http://10.15.33.11:8000/adc/get_test_execution_by_id | jq .status,.progress
585
+
586
+ # 5. Upgrade a NAC device to a specific build (out-of-band)
587
+ curl -s -X POST -H "Authorization: JWT $T" \
588
+ -F "ip=10.15.40.42" \
589
+ -F "upgrade_type=build" \
590
+ -F "build_number=7.6.6.0123" \
591
+ http://10.15.33.11:8000/adc/nac-upgrade/
592
+
593
+ # 6. Poll upgrade status
594
+ curl -s -X POST -H "Authorization: JWT $T" \
595
+ -H 'Content-Type: application/json' \
596
+ -d '{"ip":"10.15.40.42"}' \
597
+ http://10.15.33.11:8000/adc/check-upgrade-status/ | jq .
598
+ ```
599
+
600
+ ---
601
+
602
+ ## Source files
603
+
604
+ | Concern | File |
605
+ |---|---|
606
+ | URL routes | `backend/adc/urls.py` |
607
+ | Run / lab / lock | `backend/adc/views/automation/automationview.py` |
608
+ | Testbed management | `backend/adc/views/automation/automation_tbview.py` |
609
+ | FortiNAC upgrade (single device + testbed) | `backend/adc/views/sharedlab/nacviews.py` |
610
+ | Upgrade with snapshot / status polling | `backend/adc/views/sharedlab/nacupgradeview.py` |
611
+ | Status widget | `backend/adc/views/sharedlab/tbviews.py` |
612
+ | Version dropdown | `backend/adc/views/dashboard/dashboardviews.py` |
613
+ | Frontend page | `frontend/src/pages/Automation/Automation.jsx` |
614
+
615
+ When adding a new endpoint, follow the pattern: route in
616
+ `adc/urls.py` → handler in the appropriate view module → serializer
617
+ if returning a model → consumer in `Automation.jsx`.
@@ -48,6 +48,11 @@ import { randomUUID, createHash } from 'node:crypto';
48
48
  const PORT = Number(process.env.BRIDGE_PORT) || 8407;
49
49
  const FORGE_PORT = Number(process.env.PORT) || 8403;
50
50
  const RPC_TIMEOUT_MS = 60_000;
51
+ // Ceiling for per-call overrides. Kept just under undici's default 300s
52
+ // headersTimeout on the loopback fetch in bridge-client.ts — past that the
53
+ // client fetch dies first with an opaque error. Genuinely long backend work
54
+ // (multi-minute NAC upgrades) should fire-and-poll, not hold the RPC open.
55
+ const RPC_TIMEOUT_MAX_MS = 280_000;
51
56
  const TOKEN_CACHE_TTL_MS = 60_000;
52
57
 
53
58
  // ─── Forge-token validation (with short-lived cache) ──────
@@ -112,17 +117,21 @@ interface PendingRpc {
112
117
 
113
118
  const pendingRpcs = new Map<string, PendingRpc>(); // rpc_id → callbacks
114
119
 
115
- function callExtension(method: string, params: unknown): Promise<unknown> {
120
+ function callExtension(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {
116
121
  const client = pickAnyClient();
117
122
  if (!client) {
118
123
  return Promise.reject(new Error('No extension connected to the bridge.'));
119
124
  }
120
125
  const id = randomUUID();
126
+ const effectiveTimeout = Math.min(
127
+ Math.max(1000, Number(timeoutMs) || RPC_TIMEOUT_MS),
128
+ RPC_TIMEOUT_MAX_MS,
129
+ );
121
130
  return new Promise<unknown>((resolve, reject) => {
122
131
  const timer = setTimeout(() => {
123
132
  pendingRpcs.delete(id);
124
- reject(new Error(`RPC ${method} timed out after ${RPC_TIMEOUT_MS / 1000}s`));
125
- }, RPC_TIMEOUT_MS);
133
+ reject(new Error(`RPC ${method} timed out after ${effectiveTimeout / 1000}s`));
134
+ }, effectiveTimeout);
126
135
  pendingRpcs.set(id, { resolve, reject, timer });
127
136
  client.ws.send(JSON.stringify({ type: 'rpc_request', id, method, params }));
128
137
  });
@@ -248,7 +257,7 @@ async function handleHttp(req: IncomingMessage, res: ServerResponse): Promise<vo
248
257
  if (req.method === 'POST' && url.pathname === '/api/rpc') {
249
258
  try {
250
259
  const body = JSON.parse(await readBody(req));
251
- const value = await callExtension(body.method, body.params);
260
+ const value = await callExtension(body.method, body.params, body.timeout_ms);
252
261
  return sendJson(res, 200, { ok: true, value });
253
262
  } catch (e) {
254
263
  return sendJson(res, 200, { ok: false, error: (e as Error).message });