json-object-editor 0.10.668 → 0.10.670
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/TESTING_PHASE1_PROGRESS.md +216 -0
- package/_www/ai-jobs.html +218 -0
- package/_www/mcp-nav.js +21 -12
- package/_www/mcp-test.html +287 -287
- package/capp/capp.css +3 -2
- package/css/joe-ai.css +71 -0
- package/css/joe.css +1 -1
- package/docs/protocol_report_template.html +479 -0
- package/docs/protocol_report_template.js +563 -0
- package/js/JsonObjectEditor.jquery.craydent.js +22 -3
- package/js/joe-ai.js +3220 -2301
- package/js/joe.js +23 -4
- package/js/joe.min.js +1 -1
- package/package.json +1 -1
- package/pages/template.html +2 -0
- package/server/fields/core.js +16 -2
- package/server/init.js +9 -1
- package/server/modules/AiJobs.js +412 -0
- package/server/modules/MCP.js +1481 -1368
- package/server/modules/Server.js +3 -0
- package/server/plugins/chatgpt.js +2332 -2113
- package/server/schemas/ai_prompt.js +9 -2
- package/server/schemas/report.js +1 -1
- package/server/schemas/task.js +1 -0
- package/web-components/field-jobs-container.js +224 -0
package/package.json
CHANGED
package/pages/template.html
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
<link rel="stylesheet" href="${JOEPATH}js/plugins/c3/c3.min.css"/>
|
|
20
20
|
<link rel="stylesheet" href="${JOEPATH}capp/capp.css"/>
|
|
21
21
|
<link href="/JsonObjectEditor/css/joe.css" rel="stylesheet" type="text/css"/>
|
|
22
|
+
<link href="/JsonObjectEditor/css/joe-ai.css" rel="stylesheet" type="text/css"/>
|
|
22
23
|
<!--manifest.json-->
|
|
23
24
|
<link rel="manifest" href="/manifest.json">
|
|
24
25
|
|
|
@@ -341,6 +342,7 @@
|
|
|
341
342
|
</script>
|
|
342
343
|
|
|
343
344
|
<script src="${JOEPATH}js/joe-ai.js"></script>
|
|
345
|
+
<script src="${JOEPATH}web-components/field-jobs-container.js"></script>
|
|
344
346
|
<!-- <script src="https://apis.google.com/js/client.js?onload=checkGoogleAuth"></script> -->
|
|
345
347
|
</body>
|
|
346
348
|
</html>
|
package/server/fields/core.js
CHANGED
|
@@ -618,6 +618,7 @@ var fields = {
|
|
|
618
618
|
type:'content',
|
|
619
619
|
reloadable:true,
|
|
620
620
|
icon:'ai_prompt',
|
|
621
|
+
queryJobs:true,
|
|
621
622
|
run:function(obj){
|
|
622
623
|
if(!obj || !obj._id){
|
|
623
624
|
return '<joe-text>Save this item before running AI prompts.</joe-text>';
|
|
@@ -648,7 +649,13 @@ var fields = {
|
|
|
648
649
|
html += '<div class="joe-field-comment" style="margin-top:8px;">Attach files (optional)</div>';
|
|
649
650
|
html += '<select id="'+filesSelId+'" multiple class="joe-prompt-select"></select>';
|
|
650
651
|
html += '<script>(function(){ try{ _joe && _joe.Ai && _joe.Ai.renderFilesSelector && _joe.Ai.renderFilesSelector("'+filesSelId+'",{ cap:10, disableWithoutOpenAI:true }); }catch(e){} })();</script>';
|
|
651
|
-
|
|
652
|
+
// Generate progress token using centralized function
|
|
653
|
+
var progressToken = (_joe && _joe.Ai && _joe.Ai.generateProgressToken)
|
|
654
|
+
? _joe.Ai.generateProgressToken(obj._id, 'select_prompt')
|
|
655
|
+
: '';
|
|
656
|
+
html += '<joe-button class="joe-button joe-ai-button joe-iconed-button" data-progress-token="' + progressToken + '" data-field-name="select_prompt" data-object-id="' + obj._id + '" data-original-text="Run AI Prompt" onclick="_joe.Ai.runPromptSelection(this,\''+obj._id+'\',\''+selId+'\',\''+filesSelId+'\')">Run AI Prompt</joe-button>';
|
|
657
|
+
html += '<div class="joe-ai-progress" data-for-token="' + progressToken + '" style="font-size:12px; color:#666; margin-top:4px; min-height:16px;"></div>';
|
|
658
|
+
html += '<div class="joe-ai-jobs" data-for-token="' + progressToken + '" style="margin-top:4px;"></div>';
|
|
652
659
|
return html;
|
|
653
660
|
}
|
|
654
661
|
},
|
|
@@ -657,6 +664,7 @@ var fields = {
|
|
|
657
664
|
type:'content',
|
|
658
665
|
reloadable:true,
|
|
659
666
|
icon:'ai_thought',
|
|
667
|
+
queryJobs:true,
|
|
660
668
|
run:function(obj){
|
|
661
669
|
if (!obj || !obj._id) {
|
|
662
670
|
return '<joe-text>Save this item before proposing Thoughts.</joe-text>';
|
|
@@ -692,8 +700,14 @@ var fields = {
|
|
|
692
700
|
var m = String(fieldDef.model).replace(/'/g, "\\'");
|
|
693
701
|
args += ",'" + m + "'";
|
|
694
702
|
}
|
|
695
|
-
|
|
703
|
+
// Generate progress token using centralized function
|
|
704
|
+
var progressToken = (_joe && _joe.Ai && _joe.Ai.generateProgressToken)
|
|
705
|
+
? _joe.Ai.generateProgressToken(obj._id, 'proposeThought')
|
|
706
|
+
: '';
|
|
707
|
+
html += '<joe-button class="joe-button joe-ai-button joe-iconed-button" data-progress-token="' + progressToken + '" data-field-name="proposeThought" data-object-id="' + obj._id + '" data-original-text="Run Thought Agent" ';
|
|
696
708
|
html += 'onclick="_joe.Ai.runProposeThought(this,'+ args +')">Run Thought Agent</joe-button>';
|
|
709
|
+
html += '<div class="joe-ai-progress" data-for-token="' + progressToken + '" style="font-size:12px; color:#666; margin-top:4px; min-height:16px;"></div>';
|
|
710
|
+
html += '<div class="joe-ai-jobs" data-for-token="' + progressToken + '" style="margin-top:4px;"></div>';
|
|
697
711
|
return html;
|
|
698
712
|
}
|
|
699
713
|
},
|
package/server/init.js
CHANGED
|
@@ -62,7 +62,7 @@ var app = function(config){
|
|
|
62
62
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
['Apps','Schemas','Mongo','MySQL','Comments','Utils'].map(function(m){
|
|
65
|
+
['Apps','Schemas','Mongo','MySQL','Comments','Utils','AiJobs'].map(function(m){
|
|
66
66
|
initializeModule(m);
|
|
67
67
|
});
|
|
68
68
|
//initializeModule('Apps');
|
|
@@ -104,6 +104,14 @@ var app = function(config){
|
|
|
104
104
|
} catch (e) {
|
|
105
105
|
console.log('[MCP] attach error:', e && e.message);
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
// Attach AiJobs routes
|
|
109
|
+
try {
|
|
110
|
+
const AiJobs = require('./modules/AiJobs.js');
|
|
111
|
+
if (AiJobs && AiJobs.init) AiJobs.init();
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.log('[AiJobs] attach error:', e && e.message);
|
|
114
|
+
}
|
|
107
115
|
|
|
108
116
|
JOE.Sites = require('./modules/Sites.js');
|
|
109
117
|
JOE.io = require('./modules/Socket.js');
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// modules/AiJobs.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI Jobs tracking module for JOE.
|
|
5
|
+
* Tracks active AI operations (prompts, autofill, thoughts) by object and identifier.
|
|
6
|
+
* Jobs are keyed by lookup key: {objectId}_{identifier}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const AiJobs = {
|
|
10
|
+
// Active jobs storage: lookupKey -> array of job objects
|
|
11
|
+
active: {},
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Emit socket event for job update
|
|
15
|
+
* TODO: Phase 2 - Remove socket, use polling only
|
|
16
|
+
* @param {string} lookupKey - Lookup key ({objectId}_{fieldName})
|
|
17
|
+
* @param {object} job - Job object
|
|
18
|
+
*/
|
|
19
|
+
emitJobUpdate: function(lookupKey, job) {
|
|
20
|
+
// Socket removed for Phase 1 - will use polling only
|
|
21
|
+
// TODO: Remove this function in Phase 2 or re-enable if socket needed
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract lookup key from token: {objectId}|{fieldName}|{timestamp}|{userid}
|
|
26
|
+
* Returns: {objectId}_{fieldName}
|
|
27
|
+
*
|
|
28
|
+
* Examples:
|
|
29
|
+
* "objId|fieldName|123|userid" -> "objId_fieldName"
|
|
30
|
+
* "objId|description|456|userid" -> "objId_description"
|
|
31
|
+
* "objId|select_prompt|789|userid" -> "objId_select_prompt"
|
|
32
|
+
*/
|
|
33
|
+
extractKey: function(token) {
|
|
34
|
+
if (!token || typeof token !== 'string') return null;
|
|
35
|
+
// Token format: objectId|fieldName|timestamp|userid (4 parts separated by |)
|
|
36
|
+
var parts = token.split('|');
|
|
37
|
+
if (parts.length !== 4) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// New format: parts[0] = objectId, parts[1] = fieldName
|
|
41
|
+
// Return lookup key using underscore separator (consistent with API routes)
|
|
42
|
+
return parts[0] + '_' + parts[1];
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create/register a job
|
|
47
|
+
* @param {string} token - Full job token
|
|
48
|
+
* @param {object} jobData - { promptId?, promptName?, fieldId?, startTime?, status?, progress?, total?, message? }
|
|
49
|
+
* @returns {boolean} Success
|
|
50
|
+
*/
|
|
51
|
+
createJob: function(token, jobData) {
|
|
52
|
+
if (!token) return false;
|
|
53
|
+
var lookupKey = this.extractKey(token);
|
|
54
|
+
if (!lookupKey) {
|
|
55
|
+
console.warn('[AiJobs] createJob: Invalid token format:', token);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!this.active[lookupKey]) {
|
|
60
|
+
this.active[lookupKey] = [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var job = {
|
|
64
|
+
token: token,
|
|
65
|
+
startTime: jobData.startTime || new Date().toISOString(),
|
|
66
|
+
status: jobData.status || 'running',
|
|
67
|
+
promptId: jobData.promptId || null,
|
|
68
|
+
promptName: jobData.promptName || null,
|
|
69
|
+
fieldId: jobData.fieldId || null,
|
|
70
|
+
progress: jobData.progress != null ? jobData.progress : 0,
|
|
71
|
+
total: jobData.total != null ? jobData.total : null,
|
|
72
|
+
message: jobData.message || ''
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Check if job already exists (avoid duplicates)
|
|
76
|
+
var existing = this.active[lookupKey].find(function(j) {
|
|
77
|
+
return j.token === token;
|
|
78
|
+
});
|
|
79
|
+
if (existing) {
|
|
80
|
+
// Update existing job
|
|
81
|
+
Object.assign(existing, job);
|
|
82
|
+
// Emit socket update
|
|
83
|
+
this.emitJobUpdate(lookupKey, existing);
|
|
84
|
+
} else {
|
|
85
|
+
this.active[lookupKey].push(job);
|
|
86
|
+
// Emit socket update for new job
|
|
87
|
+
this.emitJobUpdate(lookupKey, job);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remove a job by token
|
|
95
|
+
* @param {string} token - Full job token
|
|
96
|
+
* @returns {boolean} Success
|
|
97
|
+
*/
|
|
98
|
+
removeJob: function(token) {
|
|
99
|
+
if (!token) return false;
|
|
100
|
+
var lookupKey = this.extractKey(token);
|
|
101
|
+
if (!lookupKey || !this.active[lookupKey]) return false;
|
|
102
|
+
|
|
103
|
+
this.active[lookupKey] = this.active[lookupKey].filter(function(j) {
|
|
104
|
+
return j.token !== token;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Clean up empty arrays
|
|
108
|
+
if (this.active[lookupKey].length === 0) {
|
|
109
|
+
delete this.active[lookupKey];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove a job with delay (updates status first, then removes after delay)
|
|
117
|
+
* @param {string} token - Full job token
|
|
118
|
+
* @param {string} finalStatus - Optional: 'error' or 'complete' (default: 'complete')
|
|
119
|
+
* @param {string} message - Optional: Final message
|
|
120
|
+
* @param {number} delaySeconds - Optional: Delay before removal in seconds (default: 10)
|
|
121
|
+
* @returns {number|null} Timeout ID (can be used to cancel), or null if job not found
|
|
122
|
+
*/
|
|
123
|
+
removeJobWithDelay: function(token, finalStatus, message, delaySeconds) {
|
|
124
|
+
if (!token) return null;
|
|
125
|
+
finalStatus = finalStatus || 'complete';
|
|
126
|
+
delaySeconds = delaySeconds != null ? delaySeconds : 10;
|
|
127
|
+
|
|
128
|
+
// Update job status first
|
|
129
|
+
var lookupKey = this.extractKey(token);
|
|
130
|
+
if (!lookupKey) return null;
|
|
131
|
+
|
|
132
|
+
var updated = this.updateJob(token, {
|
|
133
|
+
status: finalStatus,
|
|
134
|
+
message: message || (finalStatus === 'error' ? 'Error occurred' : 'Complete'),
|
|
135
|
+
progress: finalStatus === 'error' ? null : 100,
|
|
136
|
+
total: finalStatus === 'error' ? null : 100
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!updated) {
|
|
140
|
+
return null; // Job not found
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Emit final status update
|
|
144
|
+
var job = this.active[lookupKey] && this.active[lookupKey].find(function(j) {
|
|
145
|
+
return j.token === token;
|
|
146
|
+
});
|
|
147
|
+
if (job) {
|
|
148
|
+
this.emitJobUpdate(lookupKey, job);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Schedule removal after delay
|
|
152
|
+
var delayMs = delaySeconds * 1000;
|
|
153
|
+
var self = this;
|
|
154
|
+
var timeoutId = setTimeout(function() {
|
|
155
|
+
self.removeJob(token);
|
|
156
|
+
}, delayMs);
|
|
157
|
+
|
|
158
|
+
return timeoutId;
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get all active jobs for an object
|
|
163
|
+
* @param {string} objectId - Object ID
|
|
164
|
+
* @returns {array} Array of { lookupKey, jobs: [...] }
|
|
165
|
+
*/
|
|
166
|
+
getActiveJobsForObject: function(objectId) {
|
|
167
|
+
if (!objectId) return [];
|
|
168
|
+
|
|
169
|
+
var results = [];
|
|
170
|
+
for (var lookupKey in this.active) {
|
|
171
|
+
// Check if lookupKey starts with objectId_
|
|
172
|
+
if (lookupKey.indexOf(objectId + '_') === 0) {
|
|
173
|
+
var jobs = this.active[lookupKey].filter(function(j) {
|
|
174
|
+
return j.status === 'running' || j.status === 'starting';
|
|
175
|
+
});
|
|
176
|
+
if (jobs.length > 0) {
|
|
177
|
+
results.push({
|
|
178
|
+
lookupKey: lookupKey,
|
|
179
|
+
jobs: jobs
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return results;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Update job progress
|
|
189
|
+
* @param {string} token - Full job token
|
|
190
|
+
* @param {object} updates - { progress?, total?, message?, status? }
|
|
191
|
+
* @returns {boolean} Success
|
|
192
|
+
*/
|
|
193
|
+
updateJob: function(token, updates) {
|
|
194
|
+
if (!token) return false;
|
|
195
|
+
var lookupKey = this.extractKey(token);
|
|
196
|
+
if (!lookupKey || !this.active[lookupKey]) return false;
|
|
197
|
+
|
|
198
|
+
var job = this.active[lookupKey].find(function(j) {
|
|
199
|
+
return j.token === token;
|
|
200
|
+
});
|
|
201
|
+
if (job) {
|
|
202
|
+
Object.assign(job, updates);
|
|
203
|
+
// Emit socket update
|
|
204
|
+
this.emitJobUpdate(lookupKey, job);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Cleanup old jobs (safety net - call periodically)
|
|
212
|
+
* @param {number} maxAgeMinutes - Remove jobs older than this (default: 60)
|
|
213
|
+
*/
|
|
214
|
+
cleanup: function(maxAgeMinutes) {
|
|
215
|
+
maxAgeMinutes = maxAgeMinutes || 60; // Default 1 hour
|
|
216
|
+
var cutoff = new Date(Date.now() - (maxAgeMinutes * 60 * 1000)).toISOString();
|
|
217
|
+
var removed = 0;
|
|
218
|
+
|
|
219
|
+
for (var lookupKey in this.active) {
|
|
220
|
+
var before = this.active[lookupKey].length;
|
|
221
|
+
this.active[lookupKey] = this.active[lookupKey].filter(function(j) {
|
|
222
|
+
return j.startTime > cutoff;
|
|
223
|
+
});
|
|
224
|
+
removed += (before - this.active[lookupKey].length);
|
|
225
|
+
if (this.active[lookupKey].length === 0) {
|
|
226
|
+
delete this.active[lookupKey];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (removed > 0) {
|
|
231
|
+
console.log('[AiJobs] cleanup: Removed ' + removed + ' stale job(s)');
|
|
232
|
+
}
|
|
233
|
+
return removed;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Calculate elapsed seconds from startTime
|
|
238
|
+
* @param {string} startTime - ISO timestamp string
|
|
239
|
+
* @returns {number} Elapsed seconds (integer)
|
|
240
|
+
*/
|
|
241
|
+
calculateElapsed: function(startTime) {
|
|
242
|
+
if (!startTime) return 0;
|
|
243
|
+
try {
|
|
244
|
+
var start = new Date(startTime);
|
|
245
|
+
var now = new Date();
|
|
246
|
+
var elapsedMs = now - start;
|
|
247
|
+
return Math.floor(elapsedMs / 1000);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get all active jobs (for debugging/admin)
|
|
255
|
+
* @returns {object} All active jobs by lookupKey
|
|
256
|
+
*/
|
|
257
|
+
getAllActive: function() {
|
|
258
|
+
return this.active;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* HTTP Route Handler: Get all active jobs
|
|
263
|
+
*/
|
|
264
|
+
getAllActiveRoute: function(req, res) {
|
|
265
|
+
try {
|
|
266
|
+
var allJobs = [];
|
|
267
|
+
var totalCount = 0;
|
|
268
|
+
for (var lookupKey in AiJobs.active) {
|
|
269
|
+
var jobs = AiJobs.active[lookupKey].filter(function(j) {
|
|
270
|
+
return j.status === 'running' || j.status === 'starting';
|
|
271
|
+
}).map(function(j) {
|
|
272
|
+
// Add elapsed seconds to each job
|
|
273
|
+
var elapsedSeconds = AiJobs.calculateElapsed(j.startTime);
|
|
274
|
+
return Object.assign({}, j, {
|
|
275
|
+
elapsedSeconds: elapsedSeconds
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
if (jobs.length > 0) {
|
|
279
|
+
allJobs.push({
|
|
280
|
+
lookupKey: lookupKey,
|
|
281
|
+
jobs: jobs
|
|
282
|
+
});
|
|
283
|
+
totalCount += jobs.length;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return res.json({
|
|
287
|
+
jobs: allJobs,
|
|
288
|
+
count: totalCount,
|
|
289
|
+
lookupKeys: allJobs.length
|
|
290
|
+
});
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.error('[AiJobs] getAllActiveRoute error:', e);
|
|
293
|
+
return res.status(500).json({ error: e.message || 'Error getting active jobs' });
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* HTTP Route Handler: Get active jobs for specific object
|
|
299
|
+
*/
|
|
300
|
+
getActiveForObjectRoute: function(req, res) {
|
|
301
|
+
try {
|
|
302
|
+
var objectId = req.params.objectId;
|
|
303
|
+
if (!objectId) {
|
|
304
|
+
return res.status(400).json({ error: 'Object ID required' });
|
|
305
|
+
}
|
|
306
|
+
var jobs = AiJobs.getActiveJobsForObject(objectId);
|
|
307
|
+
// Add elapsed seconds to each job
|
|
308
|
+
jobs = jobs.map(function(group) {
|
|
309
|
+
return {
|
|
310
|
+
lookupKey: group.lookupKey,
|
|
311
|
+
jobs: (group.jobs || []).map(function(j) {
|
|
312
|
+
var elapsedSeconds = AiJobs.calculateElapsed(j.startTime);
|
|
313
|
+
return Object.assign({}, j, {
|
|
314
|
+
elapsedSeconds: elapsedSeconds
|
|
315
|
+
});
|
|
316
|
+
})
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
return res.json({
|
|
320
|
+
jobs: jobs,
|
|
321
|
+
objectId: objectId,
|
|
322
|
+
count: jobs.reduce(function(sum, group) {
|
|
323
|
+
return sum + (group.jobs ? group.jobs.length : 0);
|
|
324
|
+
}, 0)
|
|
325
|
+
});
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error('[AiJobs] getActiveForObjectRoute error:', e);
|
|
328
|
+
return res.status(500).json({ error: e.message || 'Error getting jobs for object' });
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* HTTP Route Handler: Get active jobs for specific object and field
|
|
334
|
+
*/
|
|
335
|
+
getActiveForFieldRoute: function(req, res) {
|
|
336
|
+
try {
|
|
337
|
+
var objectId = req.params.objectId;
|
|
338
|
+
var fieldName = req.params.fieldName;
|
|
339
|
+
if (!objectId || !fieldName) {
|
|
340
|
+
return res.status(400).json({ error: 'Object ID and field name required' });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Lookup key format: {objectId}_{fieldName}
|
|
344
|
+
var lookupKey = objectId + '_' + fieldName;
|
|
345
|
+
var jobs = [];
|
|
346
|
+
|
|
347
|
+
if (AiJobs.active[lookupKey]) {
|
|
348
|
+
jobs = AiJobs.active[lookupKey].filter(function(j) {
|
|
349
|
+
// Return all jobs (including completed) - client will filter for display
|
|
350
|
+
return true;
|
|
351
|
+
}).map(function(j) {
|
|
352
|
+
var elapsedSeconds = AiJobs.calculateElapsed(j.startTime);
|
|
353
|
+
return Object.assign({}, j, {
|
|
354
|
+
elapsedSeconds: elapsedSeconds
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return res.json({
|
|
360
|
+
jobs: jobs,
|
|
361
|
+
objectId: objectId,
|
|
362
|
+
fieldName: fieldName,
|
|
363
|
+
count: jobs.length
|
|
364
|
+
});
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.error('[AiJobs] getActiveForFieldRoute error:', e);
|
|
367
|
+
return res.status(500).json({ error: e.message || 'Error getting jobs for field' });
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Initialize routes (called from init.js after Server is ready)
|
|
373
|
+
*/
|
|
374
|
+
init: function initAiJobsRoutes() {
|
|
375
|
+
try {
|
|
376
|
+
if (!global.JOE || !JOE.Server) return;
|
|
377
|
+
if (JOE._aiJobsInitialized) return;
|
|
378
|
+
const server = JOE.Server;
|
|
379
|
+
const auth = JOE.auth; // may be undefined if no auth configured
|
|
380
|
+
|
|
381
|
+
// Get all active jobs
|
|
382
|
+
if (auth) {
|
|
383
|
+
server.get('/API/aijobs', auth, function(req, res) {
|
|
384
|
+
return AiJobs.getAllActiveRoute(req, res);
|
|
385
|
+
});
|
|
386
|
+
server.get('/API/aijobs/:objectId', auth, function(req, res) {
|
|
387
|
+
return AiJobs.getActiveForObjectRoute(req, res);
|
|
388
|
+
});
|
|
389
|
+
server.get('/API/aijobs/:objectId/:fieldName', auth, function(req, res) {
|
|
390
|
+
return AiJobs.getActiveForFieldRoute(req, res);
|
|
391
|
+
});
|
|
392
|
+
} else {
|
|
393
|
+
server.get('/API/aijobs', function(req, res) {
|
|
394
|
+
return AiJobs.getAllActiveRoute(req, res);
|
|
395
|
+
});
|
|
396
|
+
server.get('/API/aijobs/:objectId', function(req, res) {
|
|
397
|
+
return AiJobs.getActiveForObjectRoute(req, res);
|
|
398
|
+
});
|
|
399
|
+
server.get('/API/aijobs/:objectId/:fieldName', function(req, res) {
|
|
400
|
+
return AiJobs.getActiveForFieldRoute(req, res);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
JOE._aiJobsInitialized = true;
|
|
405
|
+
console.log('[AiJobs] routes attached');
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.log('[AiJobs] init error:', e);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
module.exports = AiJobs;
|