supervaizer 0.9.7__py3-none-any.whl → 0.10.0__py3-none-any.whl

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 (58) hide show
  1. supervaizer/__init__.py +11 -2
  2. supervaizer/__version__.py +1 -1
  3. supervaizer/account.py +4 -0
  4. supervaizer/account_service.py +7 -1
  5. supervaizer/admin/routes.py +46 -7
  6. supervaizer/admin/static/js/job-start-form.js +373 -0
  7. supervaizer/admin/templates/agents.html +74 -0
  8. supervaizer/admin/templates/agents_grid.html +5 -3
  9. supervaizer/admin/templates/job_start_test.html +109 -0
  10. supervaizer/admin/templates/navigation.html +11 -1
  11. supervaizer/admin/templates/supervaize_instructions.html +212 -0
  12. supervaizer/agent.py +165 -25
  13. supervaizer/case.py +46 -14
  14. supervaizer/cli.py +248 -8
  15. supervaizer/common.py +45 -4
  16. supervaizer/deploy/__init__.py +16 -0
  17. supervaizer/deploy/cli.py +296 -0
  18. supervaizer/deploy/commands/__init__.py +9 -0
  19. supervaizer/deploy/commands/clean.py +294 -0
  20. supervaizer/deploy/commands/down.py +119 -0
  21. supervaizer/deploy/commands/local.py +460 -0
  22. supervaizer/deploy/commands/plan.py +167 -0
  23. supervaizer/deploy/commands/status.py +169 -0
  24. supervaizer/deploy/commands/up.py +281 -0
  25. supervaizer/deploy/docker.py +370 -0
  26. supervaizer/deploy/driver_factory.py +42 -0
  27. supervaizer/deploy/drivers/__init__.py +39 -0
  28. supervaizer/deploy/drivers/aws_app_runner.py +607 -0
  29. supervaizer/deploy/drivers/base.py +196 -0
  30. supervaizer/deploy/drivers/cloud_run.py +570 -0
  31. supervaizer/deploy/drivers/do_app_platform.py +504 -0
  32. supervaizer/deploy/health.py +404 -0
  33. supervaizer/deploy/state.py +210 -0
  34. supervaizer/deploy/templates/Dockerfile.template +44 -0
  35. supervaizer/deploy/templates/debug_env.py +69 -0
  36. supervaizer/deploy/templates/docker-compose.yml.template +37 -0
  37. supervaizer/deploy/templates/dockerignore.template +66 -0
  38. supervaizer/deploy/templates/entrypoint.sh +20 -0
  39. supervaizer/deploy/utils.py +41 -0
  40. supervaizer/examples/{controller-template.py → controller_template.py} +5 -4
  41. supervaizer/job.py +18 -5
  42. supervaizer/job_service.py +6 -5
  43. supervaizer/parameter.py +61 -1
  44. supervaizer/protocol/__init__.py +2 -2
  45. supervaizer/protocol/a2a/routes.py +1 -1
  46. supervaizer/routes.py +262 -12
  47. supervaizer/server.py +5 -11
  48. supervaizer/utils/__init__.py +16 -0
  49. supervaizer/utils/version_check.py +56 -0
  50. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/METADATA +105 -34
  51. supervaizer-0.10.0.dist-info/RECORD +76 -0
  52. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/WHEEL +1 -1
  53. supervaizer/protocol/acp/__init__.py +0 -21
  54. supervaizer/protocol/acp/model.py +0 -198
  55. supervaizer/protocol/acp/routes.py +0 -74
  56. supervaizer-0.9.7.dist-info/RECORD +0 -50
  57. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/entry_points.txt +0 -0
  58. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/licenses/LICENSE.md +0 -0
supervaizer/__init__.py CHANGED
@@ -14,7 +14,14 @@ from supervaizer.agent import (
14
14
  AgentMethodParams,
15
15
  AgentMethods,
16
16
  )
17
- from supervaizer.case import Case, CaseNodeUpdate, CaseNoteType, Cases
17
+ from supervaizer.case import (
18
+ Case,
19
+ CaseNodeUpdate,
20
+ CaseNodeType,
21
+ Cases,
22
+ CaseNode,
23
+ CaseNodes,
24
+ )
18
25
  from supervaizer.common import ApiError, ApiResult, ApiSuccess
19
26
  from supervaizer.event import (
20
27
  AgentRegisterEvent,
@@ -51,8 +58,10 @@ __all__ = [
51
58
  "ApiSuccess",
52
59
  "Case",
53
60
  "CaseNodeUpdate",
54
- "CaseNoteType",
61
+ "CaseNodeType",
55
62
  "Cases",
63
+ "CaseNode",
64
+ "CaseNodes",
56
65
  "CaseStartEvent",
57
66
  "CaseUpdateEvent",
58
67
  "create_error_response",
@@ -5,6 +5,6 @@
5
5
  # https://mozilla.org/MPL/2.0/.
6
6
 
7
7
 
8
- VERSION = "0.9.7"
8
+ VERSION = "0.10.0"
9
9
  API_VERSION = "v1"
10
10
  TELEMETRY_VERSION = "v1"
supervaizer/account.py CHANGED
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
  from supervaizer.event import Event
21
21
  from supervaizer.job import Job
22
22
  from supervaizer.server import Server
23
+ from supervaizer.common import log
23
24
 
24
25
 
25
26
  class AccountAbstract(SvBaseModel):
@@ -268,6 +269,9 @@ class Account(AccountAbstract):
268
269
 
269
270
  def send_update_case(self, case: "Case", update: "CaseNodeUpdate") -> ApiResult:
270
271
  # Import here to avoid circular imports
272
+ log.debug(f"[send_update_case] CaseRef {case} with update {update}")
273
+ log.debug(f"[send_update_case] {type(case)}")
274
+ log.debug(f"[send_update_case] {type(update)}")
271
275
  from supervaizer.event import CaseUpdateEvent
272
276
 
273
277
  event = CaseUpdateEvent(case=case, update=update, account=self)
@@ -9,6 +9,7 @@
9
9
  # https://mozilla.org/MPL/2.0/.
10
10
 
11
11
  import logging
12
+ import os
12
13
  from typing import TYPE_CHECKING, Union
13
14
 
14
15
  import httpx
@@ -19,6 +20,11 @@ logger = logging.getLogger("httpx")
19
20
  # Enable httpx debug logging (optional - uncomment for transport-level debugging)
20
21
  logger.setLevel(logging.DEBUG)
21
22
 
23
+ _httpx_transport = httpx.HTTPTransport(
24
+ retries=int(os.getenv("SUPERVAIZE_HTTP_MAX_RETRIES", 2))
25
+ )
26
+ _httpx_client = httpx.Client(transport=_httpx_transport)
27
+
22
28
  if TYPE_CHECKING:
23
29
  from supervaizer.account import Account
24
30
  from supervaizer.agent import Agent
@@ -60,7 +66,7 @@ def send_event(
60
66
  curl_cmd = f"curl -X 'GET' '{account.url_event}' {curl_headers}"
61
67
 
62
68
  try:
63
- response = httpx.post(account.url_event, headers=headers, json=payload)
69
+ response = _httpx_client.post(account.url_event, headers=headers, json=payload)
64
70
 
65
71
  # Log response details before raising for status
66
72
 
@@ -27,7 +27,7 @@ from fastapi.templating import Jinja2Templates
27
27
  from pydantic import BaseModel
28
28
  from sse_starlette.sse import EventSourceResponse
29
29
 
30
- from supervaizer.__version__ import API_VERSION
30
+ from supervaizer.__version__ import API_VERSION, VERSION
31
31
  from supervaizer.common import log
32
32
  from supervaizer.lifecycle import EntityStatus
33
33
  from supervaizer.storage import (
@@ -257,10 +257,11 @@ def create_admin_routes() -> APIRouter:
257
257
  stats = get_dashboard_stats(storage)
258
258
 
259
259
  return templates.TemplateResponse(
260
+ request,
260
261
  "dashboard.html",
261
262
  {
262
263
  "request": request,
263
- "api_version": API_VERSION,
264
+ "api_version": VERSION,
264
265
  "stats": stats,
265
266
  "system_status": "Online",
266
267
  "db_name": "TinyDB",
@@ -276,10 +277,11 @@ def create_admin_routes() -> APIRouter:
276
277
  async def admin_jobs_page(request: Request) -> Response:
277
278
  """Jobs management page."""
278
279
  return templates.TemplateResponse(
280
+ request,
279
281
  "jobs_list.html",
280
282
  {
281
283
  "request": request,
282
- "api_version": API_VERSION,
284
+ "api_version": VERSION,
283
285
  "api_key": os.getenv("SUPERVAIZER_API_KEY"),
284
286
  },
285
287
  )
@@ -288,10 +290,11 @@ def create_admin_routes() -> APIRouter:
288
290
  async def admin_cases_page(request: Request) -> Response:
289
291
  """Cases management page."""
290
292
  return templates.TemplateResponse(
293
+ request,
291
294
  "cases_list.html",
292
295
  {
293
296
  "request": request,
294
- "api_version": API_VERSION,
297
+ "api_version": VERSION,
295
298
  "api_key": os.getenv("SUPERVAIZER_API_KEY"),
296
299
  },
297
300
  )
@@ -305,10 +308,11 @@ def create_admin_routes() -> APIRouter:
305
308
  server_config = get_server_configuration(storage)
306
309
 
307
310
  return templates.TemplateResponse(
311
+ request,
308
312
  "server.html",
309
313
  {
310
314
  "request": request,
311
- "api_version": API_VERSION,
315
+ "api_version": VERSION,
312
316
  "server_status": server_status,
313
317
  "server_config": server_config,
314
318
  "api_key": os.getenv("SUPERVAIZER_API_KEY"),
@@ -331,10 +335,11 @@ def create_admin_routes() -> APIRouter:
331
335
  )
332
336
 
333
337
  return templates.TemplateResponse(
338
+ request,
334
339
  "agents.html",
335
340
  {
336
341
  "request": request,
337
- "api_version": API_VERSION,
342
+ "api_version": VERSION,
338
343
  "agents": server_info.agents,
339
344
  "api_key": os.getenv("SUPERVAIZER_API_KEY"),
340
345
  },
@@ -345,6 +350,30 @@ def create_admin_routes() -> APIRouter:
345
350
  status_code=503, detail="Server information unavailable"
346
351
  ) from e
347
352
 
353
+ @router.get("/job-start-test", response_class=HTMLResponse)
354
+ async def admin_job_start_test_page(request: Request) -> Response:
355
+ """Job start form test page."""
356
+ return templates.TemplateResponse(
357
+ request,
358
+ "job_start_test.html",
359
+ {
360
+ "request": request,
361
+ "api_version": VERSION,
362
+ "api_key": os.getenv("SUPERVAIZER_API_KEY"),
363
+ },
364
+ )
365
+
366
+ @router.get("/static/js/job-start-form.js")
367
+ async def serve_job_start_form_js() -> Response:
368
+ """Serve the JobStartForm JavaScript file."""
369
+ js_file_path = Path(__file__).parent / "static" / "js" / "job-start-form.js"
370
+ if js_file_path.exists():
371
+ with open(js_file_path, "r") as f:
372
+ content = f.read()
373
+ return Response(content=content, media_type="application/javascript")
374
+ else:
375
+ raise HTTPException(status_code=404, detail="JavaScript file not found")
376
+
348
377
  @router.get("/console", response_class=HTMLResponse)
349
378
  async def admin_console_page(request: Request) -> Response:
350
379
  """Interactive console page - publicly accessible, authentication handled by frontend."""
@@ -355,7 +384,9 @@ def create_admin_routes() -> APIRouter:
355
384
  console_token = generate_console_token()
356
385
 
357
386
  return templates.TemplateResponse(
358
- "console.html", {"request": request, "console_token": console_token}
387
+ request,
388
+ "console.html",
389
+ {"request": request, "console_token": console_token},
359
390
  )
360
391
 
361
392
  # API Routes
@@ -371,6 +402,7 @@ def create_admin_routes() -> APIRouter:
371
402
  server_status = get_server_status()
372
403
 
373
404
  return templates.TemplateResponse(
405
+ request,
374
406
  "server_status_cards.html",
375
407
  {
376
408
  "request": request,
@@ -443,6 +475,7 @@ def create_admin_routes() -> APIRouter:
443
475
  pass
444
476
 
445
477
  return templates.TemplateResponse(
478
+ request,
446
479
  "agents_grid.html",
447
480
  {
448
481
  "request": request,
@@ -480,6 +513,7 @@ def create_admin_routes() -> APIRouter:
480
513
  raise HTTPException(status_code=404, detail="Agent not found")
481
514
 
482
515
  return templates.TemplateResponse(
516
+ request,
483
517
  "agent_detail.html",
484
518
  {
485
519
  "request": request,
@@ -563,6 +597,7 @@ def create_admin_routes() -> APIRouter:
563
597
  jobs.append(job)
564
598
 
565
599
  return templates.TemplateResponse(
600
+ request,
566
601
  "jobs_table.html",
567
602
  {
568
603
  "request": request,
@@ -591,6 +626,7 @@ def create_admin_routes() -> APIRouter:
591
626
  cases_data = storage.get_cases_for_job(job_id)
592
627
 
593
628
  return templates.TemplateResponse(
629
+ request,
594
630
  "job_detail.html",
595
631
  {
596
632
  "request": request,
@@ -681,6 +717,7 @@ def create_admin_routes() -> APIRouter:
681
717
  cases.append(case)
682
718
 
683
719
  return templates.TemplateResponse(
720
+ request,
684
721
  "cases_table.html",
685
722
  {
686
723
  "request": request,
@@ -711,6 +748,7 @@ def create_admin_routes() -> APIRouter:
711
748
  job_data = storage.get_object_by_id("Job", case_data["job_id"])
712
749
 
713
750
  return templates.TemplateResponse(
751
+ request,
714
752
  "case_detail.html",
715
753
  {
716
754
  "request": request,
@@ -862,6 +900,7 @@ def create_admin_routes() -> APIRouter:
862
900
  activities = activities[:10] # Top 10 recent activities
863
901
 
864
902
  return templates.TemplateResponse(
903
+ request,
865
904
  "recent_activity.html",
866
905
  {
867
906
  "request": request,
@@ -0,0 +1,373 @@
1
+ /**
2
+ * JobStartForm - Handles job parameter validation and submission
3
+ *
4
+ * This class provides methods to validate agent parameters and method fields
5
+ * before starting a job, using the new validation endpoints.
6
+ */
7
+
8
+ class JobStartForm {
9
+ constructor(agentPath) {
10
+ this.agentPath = agentPath;
11
+ this.form = null;
12
+ this.errorContainer = null;
13
+ this.initialize();
14
+ }
15
+
16
+ initialize() {
17
+ // Wait for DOM to be ready
18
+ if (document.readyState === 'loading') {
19
+ document.addEventListener('DOMContentLoaded', () => this.setupForm());
20
+ } else {
21
+ this.setupForm();
22
+ }
23
+ }
24
+
25
+ setupForm() {
26
+ // Find the form and error container
27
+ this.form = document.querySelector('form[data-job-start]');
28
+ this.errorContainer = document.getElementById('validation-errors');
29
+
30
+ if (!this.form) {
31
+ console.warn('JobStartForm: No form found with data-job-start attribute');
32
+ return;
33
+ }
34
+
35
+ if (!this.errorContainer) {
36
+ console.warn('JobStartForm: No validation-errors container found');
37
+ return;
38
+ }
39
+
40
+ // Add submit handler
41
+ this.form.addEventListener('submit', (e) => this.handleSubmit(e));
42
+
43
+ // Add validation on field change
44
+ this.form.addEventListener('change', () => this.clearErrors());
45
+
46
+ console.log('JobStartForm initialized for', this.agentPath);
47
+ }
48
+
49
+ async handleSubmit(event) {
50
+ event.preventDefault();
51
+
52
+ // Clear previous errors
53
+ this.clearErrors();
54
+
55
+ // Validate both agent parameters and method fields
56
+ const [agentParamsValid, methodFieldsValid] = await Promise.all([
57
+ this.validateAgentParameters(),
58
+ this.validateMethodFields()
59
+ ]);
60
+
61
+ if (agentParamsValid && methodFieldsValid) {
62
+ // All validation passed, submit the form
63
+ this.submitForm();
64
+ }
65
+ }
66
+
67
+ async validateAgentParameters() {
68
+ const encryptedParams = this.getEncryptedAgentParameters();
69
+
70
+ if (!encryptedParams) {
71
+ return true; // No agent parameters to validate
72
+ }
73
+
74
+ try {
75
+ const response = await fetch(
76
+ `${this.agentPath}/validate-agent-parameters`,
77
+ {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'X-API-Key': this.getApiKey()
82
+ },
83
+ body: JSON.stringify({
84
+ encrypted_agent_parameters: encryptedParams,
85
+ }),
86
+ }
87
+ );
88
+
89
+ const result = await response.json();
90
+
91
+ if (!result.valid) {
92
+ this.displayAgentParameterErrors(result);
93
+ return false;
94
+ }
95
+
96
+ return true;
97
+ } catch (error) {
98
+ console.error('Agent parameter validation failed:', error);
99
+ this.displayError('Agent parameter validation failed due to network error');
100
+ return false;
101
+ }
102
+ }
103
+
104
+ async validateMethodFields() {
105
+ const formData = this.getFormData();
106
+
107
+ try {
108
+ const response = await fetch(
109
+ `${this.agentPath}/validate-method-fields`,
110
+ {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ 'X-API-Key': this.getApiKey()
115
+ },
116
+ body: JSON.stringify({
117
+ method_name: 'job_start',
118
+ job_fields: formData,
119
+ }),
120
+ }
121
+ );
122
+
123
+ const result = await response.json();
124
+
125
+ if (!result.valid) {
126
+ this.displayMethodFieldErrors(result);
127
+ return false;
128
+ }
129
+
130
+ return true;
131
+ } catch (error) {
132
+ console.error('Method field validation failed:', error);
133
+ this.displayError('Method field validation failed due to network error');
134
+ return false;
135
+ }
136
+ }
137
+
138
+ getFormData() {
139
+ const formData = {};
140
+ const formElements = this.form.elements;
141
+
142
+ for (let element of formElements) {
143
+ if (element.name && element.type !== 'submit') {
144
+ if (element.type === 'checkbox') {
145
+ formData[element.name] = element.checked;
146
+ } else if (element.type === 'radio') {
147
+ if (element.checked) {
148
+ formData[element.name] = element.value;
149
+ }
150
+ } else if (element.type === 'select-multiple') {
151
+ formData[element.name] = Array.from(element.selectedOptions).map(option => option.value);
152
+ } else {
153
+ formData[element.name] = element.value;
154
+ }
155
+ }
156
+ }
157
+
158
+ return formData;
159
+ }
160
+
161
+ getEncryptedAgentParameters() {
162
+ // Look for encrypted agent parameters in hidden fields or data attributes
163
+ const encryptedField = this.form.querySelector('[name="encrypted_agent_parameters"]');
164
+ if (encryptedField) {
165
+ return encryptedField.value;
166
+ }
167
+
168
+ // Check if agent parameters are stored in data attributes
169
+ const agentParamsData = this.form.dataset.agentParameters;
170
+ if (agentParamsData) {
171
+ try {
172
+ return JSON.parse(agentParamsData);
173
+ } catch (e) {
174
+ console.warn('Failed to parse agent parameters from data attribute');
175
+ }
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ getApiKey() {
182
+ // Get API key from form data, meta tag, or global variable
183
+ const apiKeyField = this.form.querySelector('[name="api_key"]');
184
+ if (apiKeyField) {
185
+ return apiKeyField.value;
186
+ }
187
+
188
+ const metaApiKey = document.querySelector('meta[name="api-key"]');
189
+ if (metaApiKey) {
190
+ return metaApiKey.content;
191
+ }
192
+
193
+ // Check for global variable
194
+ if (typeof window.SUPERVAIZER_API_KEY !== 'undefined') {
195
+ return window.SUPERVAIZER_API_KEY;
196
+ }
197
+
198
+ return '';
199
+ }
200
+
201
+ displayAgentParameterErrors(validationResult) {
202
+ const errorSection = document.getElementById('agent-parameter-errors');
203
+ if (!errorSection) {
204
+ // Create error section if it doesn't exist
205
+ const section = document.createElement('div');
206
+ section.id = 'agent-parameter-errors';
207
+ section.className = 'mb-4';
208
+ this.errorContainer.appendChild(section);
209
+ }
210
+
211
+ const section = document.getElementById('agent-parameter-errors');
212
+ section.innerHTML = `
213
+ <div class="alert alert-warning">
214
+ <strong>Agent Configuration Issues:</strong> ${validationResult.message
215
+ }
216
+ <ul>
217
+ ${validationResult.errors
218
+ .map((error) => `<li>${error}</li>`)
219
+ .join('')}
220
+ </ul>
221
+ </div>
222
+ `;
223
+ }
224
+
225
+ displayMethodFieldErrors(validationResult) {
226
+ // Clear any existing field-specific errors
227
+ this.form.querySelectorAll('.is-invalid').forEach((field) => {
228
+ field.classList.remove('is-invalid');
229
+ });
230
+
231
+ // Display general error message
232
+ this.displayError(validationResult.message);
233
+
234
+ // Mark invalid fields and show specific error messages
235
+ Object.entries(validationResult.invalid_fields).forEach(([fieldName, errorMessage]) => {
236
+ const field = this.form.querySelector(`[name="${fieldName}"]`);
237
+ if (field) {
238
+ field.classList.add('is-invalid');
239
+
240
+ // Add error message below the field
241
+ let errorElement = field.parentNode.querySelector('.invalid-feedback');
242
+ if (!errorElement) {
243
+ errorElement = document.createElement('div');
244
+ errorElement.className = 'invalid-feedback';
245
+ field.parentNode.appendChild(errorElement);
246
+ }
247
+ errorElement.textContent = errorMessage;
248
+ }
249
+ });
250
+ }
251
+
252
+ displayError(message) {
253
+ this.errorContainer.innerHTML = `
254
+ <div class="alert alert-danger">
255
+ <strong>Validation Error:</strong> ${message}
256
+ </div>
257
+ `;
258
+ }
259
+
260
+ clearErrors() {
261
+ this.errorContainer.innerHTML = '';
262
+
263
+ // Clear agent parameter errors
264
+ const agentErrorSection = document.getElementById('agent-parameter-errors');
265
+ if (agentErrorSection) {
266
+ agentErrorSection.innerHTML = '';
267
+ }
268
+
269
+ // Clear field errors
270
+ this.form.querySelectorAll('.is-invalid').forEach((field) => {
271
+ field.classList.remove('is-invalid');
272
+ });
273
+
274
+ // Clear invalid feedback messages
275
+ this.form.querySelectorAll('.invalid-feedback').forEach((feedback) => {
276
+ feedback.remove();
277
+ });
278
+ }
279
+
280
+ async submitForm() {
281
+ try {
282
+ const formData = this.getFormData();
283
+ const encryptedParams = this.getEncryptedAgentParameters();
284
+
285
+ const requestBody = {
286
+ job_context: {
287
+ // Add any job context data here
288
+ },
289
+ job_fields: formData
290
+ };
291
+
292
+ if (encryptedParams) {
293
+ requestBody.encrypted_agent_parameters = encryptedParams;
294
+ }
295
+
296
+ const response = await fetch(`${this.agentPath}/jobs`, {
297
+ method: 'POST',
298
+ headers: {
299
+ 'Content-Type': 'application/json',
300
+ 'X-API-Key': this.getApiKey()
301
+ },
302
+ body: JSON.stringify(requestBody)
303
+ });
304
+
305
+ if (response.ok) {
306
+ const result = await response.json();
307
+ this.handleJobStarted(result);
308
+ } else {
309
+ const error = await response.json();
310
+ this.handleJobError(error);
311
+ }
312
+ } catch (error) {
313
+ console.error('Job submission failed:', error);
314
+ this.displayError('Failed to submit job due to network error');
315
+ }
316
+ }
317
+
318
+ handleJobStarted(jobData) {
319
+ // Clear form and show success message
320
+ this.form.reset();
321
+ this.clearErrors();
322
+
323
+ this.errorContainer.innerHTML = `
324
+ <div class="alert alert-success">
325
+ <strong>Job Started Successfully!</strong><br>
326
+ Job ID: ${jobData.id || 'Unknown'}<br>
327
+ Status: ${jobData.status || 'Unknown'}
328
+ </div>
329
+ `;
330
+
331
+ // Trigger any success callbacks
332
+ if (typeof this.onJobStarted === 'function') {
333
+ this.onJobStarted(jobData);
334
+ }
335
+ }
336
+
337
+ handleJobError(error) {
338
+ this.displayError(`Job submission failed: ${error.detail || error.message || 'Unknown error'}`);
339
+
340
+ // Trigger any error callbacks
341
+ if (typeof this.onJobError === 'function') {
342
+ this.onJobError(error);
343
+ }
344
+ }
345
+
346
+ // Public methods for external use
347
+ setOnJobStarted(callback) {
348
+ this.onJobStarted = callback;
349
+ }
350
+
351
+ setOnJobError(callback) {
352
+ this.onJobError = callback;
353
+ }
354
+
355
+ // Method to manually trigger validation
356
+ async validate() {
357
+ const [agentParamsValid, methodFieldsValid] = await Promise.all([
358
+ this.validateAgentParameters(),
359
+ this.validateMethodFields()
360
+ ]);
361
+ return agentParamsValid && methodFieldsValid;
362
+ }
363
+ }
364
+
365
+ // Export for module systems
366
+ if (typeof module !== 'undefined' && module.exports) {
367
+ module.exports = JobStartForm;
368
+ }
369
+
370
+ // Make available globally
371
+ if (typeof window !== 'undefined') {
372
+ window.JobStartForm = JobStartForm;
373
+ }