@flowerforce/flowerbase 1.6.3-beta.0 → 1.6.3-beta.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/monitoring/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAA;AAslB5E,QAAA,MAAM,sBAAsB,QACrB,eAAe,SACd;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,kBAw5BH,CAAA;AAE1B,eAAe,sBAAsB,CAAA"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/monitoring/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAA;AA6jB5E,QAAA,MAAM,sBAAsB,QACrB,eAAe,SACd;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,kBAg6BH,CAAA;AAE1B,eAAe,sBAAsB,CAAA"}
@@ -282,36 +282,8 @@ const buildRulesMeta = (meta) => {
282
282
  };
283
283
  };
284
284
  const buildCollectionRulesSnapshot = (rules, collection, user, runAsSystem) => {
285
- var _a, _b;
286
285
  const collectionRules = rules === null || rules === void 0 ? void 0 : rules[collection];
287
- if (!collectionRules) {
288
- return {
289
- collection,
290
- rules: null,
291
- filters: [],
292
- matchedFilters: [],
293
- roles: [],
294
- matchedRoles: [],
295
- runAsSystem: !!runAsSystem
296
- };
297
- }
298
- const filters = (_a = collectionRules.filters) !== null && _a !== void 0 ? _a : [];
299
- const roles = (_b = collectionRules.roles) !== null && _b !== void 0 ? _b : [];
300
- const safeUser = (user !== null && user !== void 0 ? user : {});
301
- const matchedFilters = (0, utils_1.getValidRule)({ filters, user: safeUser });
302
- const matchedFilterNames = matchedFilters.map((filter) => filter.name);
303
- const matchedRoles = roles
304
- .filter((role) => (0, utils_2.checkApplyWhen)(role.apply_when, safeUser, null))
305
- .map((role) => role.name);
306
- return {
307
- collection,
308
- rules: sanitize(collectionRules),
309
- filters: filters.map((filter) => filter.name),
310
- matchedFilters: matchedFilterNames,
311
- roles: roles.map((role) => role.name),
312
- matchedRoles,
313
- runAsSystem: !!runAsSystem
314
- };
286
+ return collectionRules !== null && collectionRules !== void 0 ? collectionRules : null;
315
287
  };
316
288
  const resolveAssetCandidates = (filename, prefix) => {
317
289
  const rootDir = process.cwd();
@@ -410,11 +382,11 @@ const wrapServicesForMonitoring = (addEvent) => {
410
382
  api: 'api',
411
383
  aws: 'aws',
412
384
  auth: 'auth',
413
- 'mongodb-atlas': 'rules'
385
+ 'mongodb-atlas': 'mongo'
414
386
  };
415
387
  const initMethodMap = {
416
388
  aws: new Set(['lambda', 's3']),
417
- 'mongodb-atlas': new Set(['db', 'collection'])
389
+ 'mongodb-atlas': new Set(['db', 'collection', 'limit', 'skip', 'toArray'])
418
390
  };
419
391
  const cache = new WeakMap();
420
392
  const wrapValue = (value, path, serviceName, meta) => {
@@ -854,6 +826,10 @@ const createMonitoringPlugin = (0, fastify_plugin_1.default)((app_1, ...args_1)
854
826
  });
855
827
  return { items };
856
828
  }));
829
+ app.get(`${prefix}/api/triggers`, () => __awaiter(void 0, void 0, void 0, function* () {
830
+ const triggersList = state_1.StateManager.select('triggers');
831
+ return { items: triggersList || [] };
832
+ }));
857
833
  app.get(`${prefix}/api/functions/:name`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
858
834
  var _a;
859
835
  if (!allowEdit) {
@@ -926,12 +902,15 @@ const createMonitoringPlugin = (0, fastify_plugin_1.default)((app_1, ...args_1)
926
902
  : undefined)
927
903
  }
928
904
  : undefined;
905
+ const codeModified = typeof overrideCode === 'string' && overrideCode !== currentFunction.code;
929
906
  addFunctionHistory({
930
907
  ts: Date.now(),
931
908
  name,
932
909
  args: safeArgs,
933
910
  runAsSystem: effectiveRunAsSystem,
934
- user: userInfo
911
+ user: userInfo,
912
+ code: codeModified ? overrideCode : undefined,
913
+ codeModified
935
914
  });
936
915
  addEvent({
937
916
  id: createEventId(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.6.3-beta.0",
3
+ "version": "1.6.3-beta.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,6 +31,8 @@ type FunctionHistoryItem = {
31
31
  args: unknown[]
32
32
  runAsSystem: boolean
33
33
  user?: { id?: string; email?: string }
34
+ code?: string
35
+ codeModified?: boolean
34
36
  }
35
37
 
36
38
  type CollectionHistoryItem = {
@@ -339,34 +341,7 @@ const buildCollectionRulesSnapshot = (
339
341
  runAsSystem?: boolean
340
342
  ) => {
341
343
  const collectionRules = rules?.[collection]
342
- if (!collectionRules) {
343
- return {
344
- collection,
345
- rules: null,
346
- filters: [],
347
- matchedFilters: [],
348
- roles: [],
349
- matchedRoles: [],
350
- runAsSystem: !!runAsSystem
351
- }
352
- }
353
- const filters = collectionRules.filters ?? []
354
- const roles = collectionRules.roles ?? []
355
- const safeUser = (user ?? {}) as Parameters<typeof getValidRule>[0]['user']
356
- const matchedFilters = getValidRule({ filters, user: safeUser })
357
- const matchedFilterNames = matchedFilters.map((filter) => filter.name)
358
- const matchedRoles = roles
359
- .filter((role) => checkApplyWhen(role.apply_when, safeUser as never, null))
360
- .map((role) => role.name)
361
- return {
362
- collection,
363
- rules: sanitize(collectionRules),
364
- filters: filters.map((filter) => filter.name),
365
- matchedFilters: matchedFilterNames,
366
- roles: roles.map((role) => role.name),
367
- matchedRoles,
368
- runAsSystem: !!runAsSystem
369
- }
344
+ return collectionRules ?? null
370
345
  }
371
346
 
372
347
  const resolveAssetCandidates = (filename: string, prefix: string) => {
@@ -490,11 +465,11 @@ const wrapServicesForMonitoring = (addEvent: (event: MonitorEvent) => void) => {
490
465
  api: 'api',
491
466
  aws: 'aws',
492
467
  auth: 'auth',
493
- 'mongodb-atlas': 'rules'
468
+ 'mongodb-atlas': 'mongo'
494
469
  }
495
470
  const initMethodMap: Record<string, Set<string>> = {
496
471
  aws: new Set(['lambda', 's3']),
497
- 'mongodb-atlas': new Set(['db', 'collection'])
472
+ 'mongodb-atlas': new Set(['db', 'collection', 'limit', 'skip', 'toArray'])
498
473
  }
499
474
 
500
475
  const cache = new WeakMap<object, unknown>()
@@ -972,6 +947,11 @@ const createMonitoringPlugin = fp(async (
972
947
  return { items }
973
948
  })
974
949
 
950
+ app.get(`${prefix}/api/triggers`, async () => {
951
+ const triggersList = StateManager.select('triggers') as { fileName: string; content: unknown }[] | undefined
952
+ return { items: triggersList || [] }
953
+ })
954
+
975
955
  app.get(`${prefix}/api/functions/:name`, async (req, reply) => {
976
956
  if (!allowEdit) {
977
957
  reply.code(403)
@@ -1059,12 +1039,15 @@ const createMonitoringPlugin = fp(async (
1059
1039
  : undefined)
1060
1040
  }
1061
1041
  : undefined
1042
+ const codeModified = typeof overrideCode === 'string' && overrideCode !== currentFunction.code
1062
1043
  addFunctionHistory({
1063
1044
  ts: Date.now(),
1064
1045
  name,
1065
1046
  args: safeArgs,
1066
1047
  runAsSystem: effectiveRunAsSystem,
1067
- user: userInfo
1048
+ user: userInfo,
1049
+ code: codeModified ? overrideCode : undefined,
1050
+ codeModified
1068
1051
  })
1069
1052
 
1070
1053
  addEvent({
@@ -173,6 +173,10 @@ main {
173
173
  grid-template-columns: minmax(220px, 30%) minmax(0, 1fr);
174
174
  }
175
175
 
176
+ .triggers-grid {
177
+ grid-template-columns: minmax(220px, 30%) minmax(0, 1fr);
178
+ }
179
+
176
180
  .collections-grid {
177
181
  grid-template-columns: minmax(220px, 30%) minmax(0, 1fr);
178
182
  }
@@ -276,6 +280,12 @@ button {
276
280
  cursor: pointer;
277
281
  }
278
282
 
283
+ button:disabled {
284
+ opacity: 0.45;
285
+ cursor: not-allowed;
286
+ filter: grayscale(0.2);
287
+ }
288
+
279
289
  button.secondary {
280
290
  color: var(--muted);
281
291
  border-color: var(--border);
@@ -301,7 +311,7 @@ button.danger {
301
311
 
302
312
  .event-row {
303
313
  display: grid;
304
- grid-template-columns: 86px 90px 1fr;
314
+ grid-template-columns: 86px 90px 160px 1fr;
305
315
  gap: 8px;
306
316
  padding: 4px 6px;
307
317
  border-bottom: 1px dashed rgba(26, 47, 34, 0.6);
@@ -328,6 +338,22 @@ button.danger {
328
338
  color: var(--warn);
329
339
  }
330
340
 
341
+ .event-user {
342
+ color: var(--muted);
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ white-space: nowrap;
346
+ }
347
+
348
+ .event-user.is-link {
349
+ color: var(--accent);
350
+ cursor: pointer;
351
+ }
352
+
353
+ .event-user.is-link:hover {
354
+ text-decoration: underline;
355
+ }
356
+
331
357
  .event-detail {
332
358
  margin-top: 10px;
333
359
  background: var(--panel);
@@ -416,6 +442,26 @@ button.small {
416
442
  background: rgba(49, 233, 129, 0.08);
417
443
  }
418
444
 
445
+ .history-modified {
446
+ color: var(--warn);
447
+ text-transform: uppercase;
448
+ letter-spacing: 0.6px;
449
+ font-size: 10px;
450
+ }
451
+
452
+ .modified-pill {
453
+ display: inline-flex;
454
+ align-items: center;
455
+ padding: 1px 6px;
456
+ border-radius: 999px;
457
+ font-size: 10px;
458
+ text-transform: uppercase;
459
+ letter-spacing: 0.6px;
460
+ color: var(--warn);
461
+ border: 1px solid rgba(255, 176, 64, 0.5);
462
+ background: rgba(255, 176, 64, 0.12);
463
+ }
464
+
419
465
  .history-meta {
420
466
  display: flex;
421
467
  flex-direction: column;
@@ -462,6 +508,10 @@ button.small {
462
508
  border-color: rgba(49, 233, 129, 0.45);
463
509
  }
464
510
 
511
+ .user-row.disabled {
512
+ opacity: 0.55;
513
+ }
514
+
465
515
  .user-row:hover {
466
516
  background: rgba(49, 233, 129, 0.08);
467
517
  }
@@ -500,6 +550,16 @@ button.small {
500
550
  cursor: pointer;
501
551
  }
502
552
 
553
+ .trigger-row {
554
+ display: flex;
555
+ align-items: center;
556
+ justify-content: space-between;
557
+ gap: 8px;
558
+ padding: 6px;
559
+ border-bottom: 1px dashed rgba(26, 47, 34, 0.6);
560
+ cursor: pointer;
561
+ }
562
+
503
563
  .function-row.active {
504
564
  background: rgba(49, 233, 129, 0.12);
505
565
  border-color: rgba(49, 233, 129, 0.35);
@@ -509,6 +569,24 @@ button.small {
509
569
  background: rgba(49, 233, 129, 0.08);
510
570
  }
511
571
 
572
+ .trigger-row.active {
573
+ background: rgba(49, 233, 129, 0.12);
574
+ border-color: rgba(49, 233, 129, 0.35);
575
+ }
576
+
577
+ .trigger-row:hover {
578
+ background: rgba(49, 233, 129, 0.08);
579
+ }
580
+
581
+ .trigger-link {
582
+ color: var(--accent);
583
+ cursor: pointer;
584
+ }
585
+
586
+ .trigger-link:hover {
587
+ text-decoration: underline;
588
+ }
589
+
512
590
  .user-meta {
513
591
  display: flex;
514
592
  flex-direction: column;
@@ -549,6 +627,26 @@ button.small {
549
627
  min-height: 0;
550
628
  }
551
629
 
630
+ .collection-actions {
631
+ display: flex;
632
+ flex-wrap: wrap;
633
+ align-items: center;
634
+ justify-content: space-between;
635
+ gap: 10px;
636
+ margin-top: 20px;
637
+ }
638
+
639
+ .collection-actions .editor-actions {
640
+ display: flex;
641
+ flex-wrap: wrap;
642
+ align-items: center;
643
+ gap: 12px;
644
+ }
645
+
646
+ .collection-actions .result-toolbar {
647
+ margin-left: auto;
648
+ }
649
+
552
650
  .mini-tabs {
553
651
  display: flex;
554
652
  gap: 8px;
@@ -597,22 +695,46 @@ button.small {
597
695
  min-height: 0;
598
696
  }
599
697
 
600
- .collection-controls {
698
+ .collection-layout {
601
699
  display: grid;
602
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
700
+ grid-template-columns: minmax(200px, 30%) minmax(0, 70%);
701
+ gap: 12px;
702
+ align-items: start;
703
+ }
704
+
705
+ .collection-controls {
706
+ display: flex;
707
+ flex-direction: column;
603
708
  gap: 10px;
604
- align-items: end;
605
709
  }
606
710
 
607
711
  .collection-io {
608
712
  display: grid;
609
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
713
+ grid-template-columns: minmax(0, 1fr);
610
714
  gap: 12px;
611
715
  align-items: stretch;
612
716
  }
613
717
 
718
+ .collection-io[data-mode="query"] [data-collection-mode="aggregate"] {
719
+ display: none;
720
+ }
721
+
722
+ .collection-io[data-mode="aggregate"] [data-collection-mode="query"] {
723
+ display: none;
724
+ }
725
+
726
+ .collection-io {
727
+ height: 100%;
728
+ }
729
+
614
730
  .collection-io textarea {
615
731
  min-height: 140px;
732
+ height: 100%;
733
+ }
734
+
735
+ .collection-io .json-editor {
736
+ height: 100%;
737
+ min-height: 100%;
616
738
  }
617
739
 
618
740
  .json-editor {
@@ -977,7 +1099,7 @@ button.small {
977
1099
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
978
1100
  }
979
1101
 
980
- .collection-controls {
1102
+ .collection-layout {
981
1103
  grid-template-columns: minmax(0, 1fr);
982
1104
  }
983
- }
1105
+ }
@@ -26,6 +26,7 @@
26
26
  <button class="tab-button active" data-tab="events">events</button>
27
27
  <button class="tab-button" data-tab="users">users</button>
28
28
  <button class="tab-button" data-tab="functions">functions</button>
29
+ <button class="tab-button" data-tab="triggers">triggers</button>
29
30
  <button class="tab-button" data-tab="collections">collections</button>
30
31
  </div>
31
32
  <div class="tab-panels">
@@ -46,7 +47,7 @@
46
47
  <option value="http_endpoint">http_endpoint</option>
47
48
  <option value="api">api</option>
48
49
  <option value="aws">aws</option>
49
- <option value="rules">rules</option>
50
+ <option value="mongo">mongo</option>
50
51
  <option value="log">log</option>
51
52
  <option value="error">error</option>
52
53
  </select>
@@ -73,7 +74,7 @@
73
74
  <div class="controls">
74
75
  <input id="userSearch" class="wide-input" type="text" placeholder="search users (email, id, status)" />
75
76
  </div>
76
- <div class="split-grid users-grid">
77
+ <div class="split-grid users-grid functions-grid">
77
78
  <div class="column-stack">
78
79
  <div class="subpanel list-panel">
79
80
  <div class="subpanel-title">users</div>
@@ -166,7 +167,6 @@
166
167
  <button id="restoreFunction" class="secondary">restore original</button>
167
168
  <button id="invokeFunction">invoke</button>
168
169
  </div>
169
- <div class="hint" id="functionEditorStatus"></div>
170
170
  </div>
171
171
  <div class="function-io">
172
172
  <div class="io-column">
@@ -182,6 +182,29 @@
182
182
  </div>
183
183
  </div>
184
184
  </section>
185
+ <section class="panel tab-panel" data-panel="triggers">
186
+ <div class="panel-header">
187
+ <span>Triggers</span>
188
+ <button id="refreshTriggers" class="secondary">refresh</button>
189
+ </div>
190
+ <div class="split-grid triggers-grid">
191
+ <div class="column-stack">
192
+ <div class="subpanel list-panel">
193
+ <div class="subpanel-title">triggers</div>
194
+ <div id="triggerList" class="user-list"></div>
195
+ </div>
196
+ </div>
197
+ <div class="subpanel detail-panel">
198
+ <div class="detail-header">
199
+ <div class="subpanel-title">trigger detail</div>
200
+ <button id="triggerFunctionButton" type="button" class="secondary small is-hidden">
201
+ go to function
202
+ </button>
203
+ </div>
204
+ <pre id="triggerDetail" class="event-detail">select a trigger to inspect</pre>
205
+ </div>
206
+ </div>
207
+ </section>
185
208
  <section class="panel tab-panel" data-panel="collections">
186
209
  <div class="panel-header">
187
210
  <span>Collections</span>
@@ -213,61 +236,65 @@
213
236
  </div>
214
237
  <div class="mini-panels">
215
238
  <div class="mini-panel active" data-collection-panel="query">
216
- <div class="collection-controls">
217
- <div class="control-group">
218
- <label for="collectionUserInput">user</label>
219
- <input id="collectionUserInput" list="collectionUserList" placeholder="select user" />
220
- <datalist id="collectionUserList"></datalist>
221
- </div>
222
- <div class="control-group">
223
- <label for="collectionRunMode">run as</label>
224
- <select id="collectionRunMode">
225
- <option value="system" selected>system</option>
226
- <option value="user">user</option>
227
- </select>
228
- </div>
229
- <div class="control-group">
230
- <label for="collectionMode">mode</label>
231
- <select id="collectionMode">
232
- <option value="query" selected>query</option>
233
- <option value="aggregate">aggregate</option>
234
- </select>
235
- </div>
236
- <div class="control-group">
237
- <label for="collectionSort">sort</label>
238
- <input id="collectionSort" type="text" placeholder='{"createdAt": -1}' />
239
- </div>
240
- </div>
241
- <div class="collection-io">
242
- <div class="io-column">
243
- <label for="collectionQuery">query</label>
244
- <div class="json-editor">
245
- <pre id="collectionQueryHighlight" class="json-highlight"></pre>
246
- <textarea id="collectionQuery" placeholder='{"field": "value"}'></textarea>
239
+ <div class="collection-layout">
240
+ <div class="collection-controls">
241
+ <div class="control-group">
242
+ <label for="collectionUserInput">user</label>
243
+ <input id="collectionUserInput" list="collectionUserList" placeholder="select user" />
244
+ <datalist id="collectionUserList"></datalist>
245
+ </div>
246
+ <div class="control-group">
247
+ <label for="collectionRunMode">run as</label>
248
+ <select id="collectionRunMode">
249
+ <option value="system" selected>system</option>
250
+ <option value="user">user</option>
251
+ </select>
252
+ </div>
253
+ <div class="control-group">
254
+ <label for="collectionMode">mode</label>
255
+ <select id="collectionMode">
256
+ <option value="query" selected>query</option>
257
+ <option value="aggregate">aggregate</option>
258
+ </select>
259
+ </div>
260
+ <div class="control-group">
261
+ <label for="collectionSort">sort</label>
262
+ <input id="collectionSort" type="text" placeholder='{"createdAt": -1}' />
263
+ </div>
247
264
  </div>
248
- </div>
249
- <div class="io-column">
250
- <label for="collectionAggregate">aggregate</label>
251
- <div class="json-editor">
252
- <pre id="collectionAggregateHighlight" class="json-highlight"></pre>
253
- <textarea id="collectionAggregate" placeholder='[{ "$match": { "field": "value" } }]'></textarea>
265
+ <div id="collectionIo" class="collection-io" data-mode="query">
266
+ <div class="io-column" data-collection-mode="query">
267
+ <label for="collectionQuery">query</label>
268
+ <div class="json-editor">
269
+ <pre id="collectionQueryHighlight" class="json-highlight"></pre>
270
+ <textarea id="collectionQuery" placeholder='{"field": "value"}'></textarea>
271
+ </div>
272
+ </div>
273
+ <div class="io-column" data-collection-mode="aggregate">
274
+ <label for="collectionAggregate">aggregate</label>
275
+ <div class="json-editor">
276
+ <pre id="collectionAggregateHighlight" class="json-highlight"></pre>
277
+ <textarea id="collectionAggregate" placeholder='[{ "$match": { "field": "value" } }]'></textarea>
278
+ </div>
279
+ </div>
254
280
  </div>
255
281
  </div>
256
- </div>
257
- <div class="editor-actions">
258
- <button id="runCollectionQuery">run</button>
259
- <div class="pager">
260
- <div class="pager-controls">
261
- <button id="collectionPrev" class="secondary">prev</button>
262
- <button id="collectionNext" class="secondary">next</button>
282
+ <div class="collection-actions">
283
+ <div class="editor-actions">
284
+ <button id="runCollectionQuery">run</button>
285
+ <div class="pager">
286
+ <div class="pager-controls">
287
+ <button id="collectionPrev" class="secondary">prev</button>
288
+ <button id="collectionNext" class="secondary">next</button>
289
+ </div>
290
+ <div>page <span id="collectionPage">1</span>/<span id="collectionPages">1</span> · total <span id="collectionTotal">0</span></div>
263
291
  </div>
264
- <div>page <span id="collectionPage">1</span> · total <span id="collectionTotal">0</span></div>
265
292
  </div>
266
- </div>
267
- <div class="result-toolbar">
268
- <div class="toggle-buttons">
269
- <button id="collectionViewJson" class="secondary active">json</button>
270
- <button id="collectionViewTable" class="secondary">table</button>
293
+ <div class="result-toolbar">
294
+ <div class="toggle-buttons">
295
+ <button id="collectionViewJson" class="secondary active">json</button>
296
+ <button id="collectionViewTable" class="secondary">table</button>
297
+ </div>
271
298
  </div>
272
299
  </div>
273
300
  <div id="collectionResult" class="event-detail function-result"></div>
@@ -19,6 +19,9 @@
19
19
  functionUserMap: {},
20
20
  functionUserQuery: '',
21
21
  __functionUserTimer: null,
22
+ triggers: [],
23
+ selectedTrigger: null,
24
+ triggerFunctionMap: {},
22
25
  collections: [],
23
26
  selectedCollection: null,
24
27
  collectionSearch: '',
@@ -28,6 +31,9 @@
28
31
  collectionPage: 1,
29
32
  collectionHasMore: false,
30
33
  collectionTotal: 0,
34
+ collectionPageSize: 50,
35
+ collectionLoading: false,
36
+ collectionTotalsLoading: false,
31
37
  collectionResultView: 'json',
32
38
  collectionResultPayload: null,
33
39
  collectionResultHighlight: false,
@@ -74,18 +80,22 @@
74
80
  const functionHighlight = document.getElementById('functionHighlight');
75
81
  const functionGutter = document.getElementById('functionGutter');
76
82
  const restoreFunction = document.getElementById('restoreFunction');
77
- const functionEditorStatus = document.getElementById('functionEditorStatus');
78
83
  const functionArgs = document.getElementById('functionArgs');
79
84
  const invokeFunction = document.getElementById('invokeFunction');
80
85
  const functionResult = document.getElementById('functionResult');
81
86
  const refreshFunctions = document.getElementById('refreshFunctions');
87
+ const refreshTriggers = document.getElementById('refreshTriggers');
82
88
  const functionHistory = document.getElementById('functionHistory');
89
+ const triggerList = document.getElementById('triggerList');
90
+ const triggerDetail = document.getElementById('triggerDetail');
91
+ const triggerFunctionButton = document.getElementById('triggerFunctionButton');
83
92
  const refreshCollections = document.getElementById('refreshCollections');
84
93
  const collectionSearch = document.getElementById('collectionSearch');
85
94
  const collectionList = document.getElementById('collectionList');
86
95
  const collectionHistory = document.getElementById('collectionHistory');
87
96
  const collectionRules = document.getElementById('collectionRules');
88
97
  const collectionSelected = document.getElementById('collectionSelected');
98
+ const collectionIo = document.getElementById('collectionIo');
89
99
  const collectionUserInput = document.getElementById('collectionUserInput');
90
100
  const collectionUserList = document.getElementById('collectionUserList');
91
101
  const collectionRunMode = document.getElementById('collectionRunMode');
@@ -100,6 +110,7 @@
100
110
  const collectionPrev = document.getElementById('collectionPrev');
101
111
  const collectionNext = document.getElementById('collectionNext');
102
112
  const collectionPage = document.getElementById('collectionPage');
113
+ const collectionPages = document.getElementById('collectionPages');
103
114
  const collectionTotal = document.getElementById('collectionTotal');
104
115
  const collectionViewJson = document.getElementById('collectionViewJson');
105
116
  const collectionViewTable = document.getElementById('collectionViewTable');
@@ -172,9 +183,26 @@
172
183
  };
173
184
 
174
185
  const updateCollectionPager = () => {
186
+ if (state.collectionLoading) {
187
+ if (collectionPage) collectionPage.textContent = '...';
188
+ if (state.collectionTotalsLoading) {
189
+ if (collectionPages) collectionPages.textContent = '...';
190
+ if (collectionTotal) collectionTotal.textContent = '...';
191
+ }
192
+ if (collectionPrev) collectionPrev.disabled = true;
193
+ if (collectionNext) collectionNext.disabled = true;
194
+ if (state.collectionTotalsLoading) return;
195
+ }
175
196
  if (collectionPage) {
176
197
  collectionPage.textContent = String(state.collectionPage || 1);
177
198
  }
199
+ const totalPages = Math.max(
200
+ 1,
201
+ Math.ceil((state.collectionTotal || 0) / Math.max(state.collectionPageSize || 1, 1))
202
+ );
203
+ if (collectionPages) {
204
+ collectionPages.textContent = String(totalPages);
205
+ }
178
206
  if (collectionTotal) {
179
207
  collectionTotal.textContent = String(state.collectionTotal || 0);
180
208
  }
@@ -503,6 +531,40 @@
503
531
  return time + ' ' + type.padEnd(12, ' ') + ' ' + msg;
504
532
  };
505
533
 
534
+ const getEventUserId = (event) => {
535
+ if (!event || typeof event !== 'object') return '';
536
+ if (typeof event.userId === 'string') return event.userId;
537
+ if (event.user && typeof event.user === 'object') {
538
+ if (typeof event.user.id === 'string') return event.user.id;
539
+ if (typeof event.user._id === 'string') return event.user._id;
540
+ if (typeof event.user.userId === 'string') return event.user.userId;
541
+ }
542
+ const data = event.data;
543
+ if (!data || typeof data !== 'object') return '';
544
+ if (typeof data.userId === 'string') return data.userId;
545
+ if (typeof data.user_id === 'string') return data.user_id;
546
+ if (data.user && typeof data.user === 'object') {
547
+ if (typeof data.user.id === 'string') return data.user.id;
548
+ if (typeof data.user._id === 'string') return data.user._id;
549
+ if (typeof data.user.userId === 'string') return data.user.userId;
550
+ }
551
+ if (data.user_data && typeof data.user_data === 'object') {
552
+ if (typeof data.user_data.id === 'string') return data.user_data.id;
553
+ if (typeof data.user_data._id === 'string') return data.user_data._id;
554
+ }
555
+ return '';
556
+ };
557
+
558
+ const goToUser = (userId) => {
559
+ if (!userId || !userSearch) return;
560
+ state.selectedUserId = String(userId);
561
+ state.userQuery = state.selectedUserId;
562
+ state.customPage = 1;
563
+ userSearch.value = state.userQuery;
564
+ setActiveTab('users');
565
+ loadUsers();
566
+ };
567
+
506
568
  const getFunctionEventData = (event) => {
507
569
  if (!event || !event.type || event.type !== 'function') return null;
508
570
  const data = event.data || {};
@@ -542,16 +604,28 @@
542
604
  if (type && event.type !== type) return false;
543
605
  return matchesQuery(event, query);
544
606
  });
545
- const recent = filtered.slice(-350);
607
+ const recent = filtered.slice(-350).reverse();
546
608
  eventsList.innerHTML = '';
547
609
  recent.forEach((event) => {
610
+ const userId = getEventUserId(event);
548
611
  const row = document.createElement('div');
549
612
  row.className = 'event-row';
550
613
  row.dataset.id = event.id;
551
614
  const typeClass = event.type === 'error' ? 'error' : (event.type === 'warn' ? 'warn' : '');
552
615
  row.innerHTML = '<div>' + formatTime(event.ts) + '</div>' +
553
616
  '<div class="event-type ' + typeClass + '">' + (event.type || '-') + '</div>' +
617
+ '<div class="event-user" title="' + (userId || '-') + '">' + (userId || '-') + '</div>' +
554
618
  '<div>' + (event.message || '') + '</div>';
619
+ if (userId) {
620
+ const userCell = row.querySelector('.event-user');
621
+ if (userCell) {
622
+ userCell.classList.add('is-link');
623
+ userCell.addEventListener('click', (clickEvent) => {
624
+ clickEvent.stopPropagation();
625
+ goToUser(userId);
626
+ });
627
+ }
628
+ }
555
629
  row.addEventListener('click', () => showDetail(event));
556
630
  eventsList.appendChild(row);
557
631
  });
@@ -560,7 +634,9 @@
560
634
  const showDetail = (event) => {
561
635
  state.selectedId = event.id;
562
636
  state.selectedEvent = event;
563
- eventDetail.textContent = JSON.stringify(event, null, 2);
637
+ const text = JSON.stringify(event, null, 2) || '';
638
+ eventDetail.classList.add('json-highlight');
639
+ eventDetail.innerHTML = highlightJson(text);
564
640
  const functionData = getFunctionEventData(event);
565
641
  if (functionData && eventFunctionButton) {
566
642
  eventFunctionButton.classList.remove('is-hidden');
@@ -694,7 +770,10 @@
694
770
  const hint = createdLabel ? status + ' · ' + createdLabel : status;
695
771
  const hasAuth = !!(auth && auth._id);
696
772
  const row = document.createElement('div');
697
- row.className = 'user-row' + (state.selectedUserId === userId ? ' active' : '');
773
+ const isDisabled = auth && auth.status === 'disabled';
774
+ row.className = 'user-row' +
775
+ (state.selectedUserId === userId ? ' active' : '') +
776
+ (isDisabled ? ' disabled' : '');
698
777
  row.dataset.id = userId;
699
778
  row.innerHTML = '<div class="user-meta">' +
700
779
  '<div class="code">' + primaryEmail + '</div>' +
@@ -754,12 +833,112 @@
754
833
  row.dataset.name = fn.name;
755
834
  const runMode = fn.run_as_system ? 'system' : 'user';
756
835
  const visibility = fn.private ? 'private' : 'public';
836
+ const triggerName = state.triggerFunctionMap ? state.triggerFunctionMap[fn.name] : null;
837
+ const metaParts = [visibility, runMode];
838
+ const triggerTag = triggerName
839
+ ? ' · <span class="trigger-link" data-trigger="' + escapeHtml(triggerName) + '">isTrigger</span>'
840
+ : '';
757
841
  row.innerHTML = '<div class="code">' + fn.name + '</div>' +
758
- '<div class="hint">' + visibility + ' · ' + runMode + '</div>';
842
+ '<div class="hint">' + metaParts.join(' · ') + triggerTag + '</div>';
759
843
  functionList.appendChild(row);
760
844
  });
761
845
  };
762
846
 
847
+ const buildTriggerFunctionMap = (items) => {
848
+ const map = {};
849
+ (items || []).forEach((entry) => {
850
+ const content = entry && entry.content ? entry.content : null;
851
+ const handler = content && content.event_processors ? content.event_processors.FUNCTION : null;
852
+ const fnName = handler && handler.config ? handler.config.function_name : null;
853
+ const triggerName = (content && content.name) || entry.fileName || 'trigger';
854
+ if (typeof fnName === 'string' && fnName) {
855
+ map[fnName] = triggerName;
856
+ }
857
+ });
858
+ state.triggerFunctionMap = map;
859
+ };
860
+
861
+ const renderTriggers = (items) => {
862
+ if (!triggerList) return;
863
+ triggerList.innerHTML = '';
864
+ if (!items || !items.length) {
865
+ const empty = document.createElement('div');
866
+ empty.className = 'history-empty';
867
+ empty.textContent = 'no triggers yet';
868
+ triggerList.appendChild(empty);
869
+ return;
870
+ }
871
+ items.forEach((entry) => {
872
+ const content = entry && entry.content ? entry.content : null;
873
+ const name = (content && content.name) || entry.fileName || 'trigger';
874
+ const type = content && content.type ? content.type : 'unknown';
875
+ const handler = content && content.event_processors ? content.event_processors.FUNCTION : null;
876
+ const fnName = handler && handler.config ? handler.config.function_name : null;
877
+ const hint = fnName ? (type + ' · ' + fnName) : type;
878
+ const row = document.createElement('div');
879
+ row.className = 'trigger-row' + (state.selectedTrigger === entry ? ' active' : '');
880
+ row.dataset.name = name;
881
+ row.innerHTML = '<div class="code">' + name + '</div>' +
882
+ '<div class="hint">' + hint + '</div>';
883
+ triggerList.appendChild(row);
884
+ });
885
+ };
886
+
887
+ const showTriggerDetail = (entry) => {
888
+ state.selectedTrigger = entry;
889
+ const content = entry && entry.content ? entry.content : null;
890
+ const handler = content && content.event_processors ? content.event_processors.FUNCTION : null;
891
+ const fnName = handler && handler.config ? handler.config.function_name : null;
892
+ if (triggerList) {
893
+ triggerList.querySelectorAll('.trigger-row').forEach((row) => {
894
+ const name = row.dataset.name;
895
+ const currentName = (content && content.name) || entry.fileName || 'trigger';
896
+ row.classList.toggle('active', name === currentName);
897
+ });
898
+ }
899
+ if (triggerDetail) {
900
+ triggerDetail.classList.add('json-highlight');
901
+ triggerDetail.innerHTML = highlightJson(JSON.stringify(entry, null, 2) || '');
902
+ }
903
+ if (triggerFunctionButton) {
904
+ if (fnName) {
905
+ triggerFunctionButton.classList.remove('is-hidden');
906
+ triggerFunctionButton.dataset.functionName = fnName;
907
+ } else {
908
+ triggerFunctionButton.classList.add('is-hidden');
909
+ triggerFunctionButton.dataset.functionName = '';
910
+ }
911
+ }
912
+ };
913
+
914
+ const loadTriggers = async () => {
915
+ try {
916
+ const data = await api('/triggers');
917
+ state.triggers = data.items || [];
918
+ buildTriggerFunctionMap(state.triggers);
919
+ renderTriggers(state.triggers);
920
+ renderFunctions(state.functions);
921
+ return state.triggers;
922
+ } catch (err) {
923
+ console.error(err);
924
+ return [];
925
+ }
926
+ };
927
+
928
+ const openTriggerByName = async (triggerName) => {
929
+ if (!triggerName) return;
930
+ setActiveTab('triggers');
931
+ const entries = state.triggers && state.triggers.length ? state.triggers : await loadTriggers();
932
+ const entry = (entries || []).find((item) => {
933
+ const content = item && item.content ? item.content : null;
934
+ const name = (content && content.name) || item.fileName || 'trigger';
935
+ return name === triggerName;
936
+ });
937
+ if (entry) {
938
+ showTriggerDetail(entry);
939
+ }
940
+ };
941
+
763
942
  const formatArgsPreview = (args) => {
764
943
  try {
765
944
  let preview = JSON.stringify(args);
@@ -794,9 +973,11 @@
794
973
  metaParts.push(formatTime(entry.ts));
795
974
  metaParts.push(runMode);
796
975
  if (userLabel) metaParts.push(userLabel);
976
+ const modifiedTag = entry.codeModified ? '<span class="modified-pill">modified</span>' : '';
797
977
  row.innerHTML = '<div class="history-meta">' +
798
978
  '<div class="code">' + entry.name + '</div>' +
799
- '<div class="hint">' + metaParts.join(' · ') + ' · ' + formatArgsPreview(entry.args) + '</div>' +
979
+ '<div class="hint">' + metaParts.join(' · ') + ' · ' + formatArgsPreview(entry.args) +
980
+ (modifiedTag ? (' · ' + modifiedTag) : '') + '</div>' +
800
981
  '</div>' +
801
982
  '<div class="hint"></div>';
802
983
  functionHistory.appendChild(row);
@@ -821,10 +1002,34 @@
821
1002
  functionRunMode.value = fn.run_as_system ? 'system' : 'user';
822
1003
  };
823
1004
 
824
- const setEditorStatus = (text, isError) => {
825
- if (!functionEditorStatus) return;
826
- functionEditorStatus.textContent = text || '';
827
- functionEditorStatus.classList.toggle('error', !!isError);
1005
+ const setFunctionSelectedLabel = (name, modified) => {
1006
+ if (!functionSelected) return;
1007
+ if (!name) {
1008
+ functionSelected.textContent = 'select a function';
1009
+ return;
1010
+ }
1011
+ if (modified) {
1012
+ functionSelected.innerHTML =
1013
+ escapeHtml(name) + ' <span class="modified-pill">modified</span>';
1014
+ } else {
1015
+ functionSelected.textContent = name;
1016
+ }
1017
+ };
1018
+
1019
+ const isFunctionCodeModified = (name) => {
1020
+ if (!name || !functionCode) return false;
1021
+ const base = state.functionCodeCache[name];
1022
+ if (typeof base !== 'string') return false;
1023
+ return functionCode.value !== base;
1024
+ };
1025
+
1026
+ const updateFunctionModifiedState = () => {
1027
+ const name = state.selectedFunction;
1028
+ const modified = isFunctionCodeModified(name);
1029
+ setFunctionSelectedLabel(name, modified);
1030
+ if (restoreFunction) {
1031
+ restoreFunction.disabled = !modified;
1032
+ }
828
1033
  };
829
1034
 
830
1035
  const loadFunctionCode = async () => {
@@ -833,20 +1038,26 @@
833
1038
  if (!name) {
834
1039
  functionCode.value = '';
835
1040
  updateFunctionEditor();
836
- setEditorStatus('Select a function first', true);
1041
+ updateFunctionModifiedState();
837
1042
  return;
838
1043
  }
839
1044
  try {
840
- setEditorStatus('loading...');
841
1045
  const data = await api('/functions/' + encodeURIComponent(name));
842
1046
  const baseCode = data && data.code ? data.code : '';
843
1047
  state.functionCodeCache[name] = baseCode;
844
1048
  functionCode.value = baseCode;
845
1049
  updateFunctionEditor();
846
- setEditorStatus('loaded');
847
1050
  } catch (err) {
848
- setEditorStatus('Error: ' + err.message, true);
1051
+ console.error(err);
849
1052
  }
1053
+ updateFunctionModifiedState();
1054
+ };
1055
+
1056
+ const applyFunctionOverride = (code) => {
1057
+ if (!functionCode) return;
1058
+ functionCode.value = code || '';
1059
+ updateFunctionEditor();
1060
+ updateFunctionModifiedState();
850
1061
  };
851
1062
 
852
1063
  const clearFunctionOverride = () => {
@@ -856,7 +1067,7 @@
856
1067
  functionCode.value = state.functionCodeCache[name] || '';
857
1068
  updateFunctionEditor();
858
1069
  }
859
- setEditorStatus('override cleared');
1070
+ updateFunctionModifiedState();
860
1071
  };
861
1072
 
862
1073
  const buildFunctionUserOptions = (authItems, customItems) => {
@@ -1011,6 +1222,17 @@
1011
1222
  }
1012
1223
  };
1013
1224
 
1225
+ const updateCollectionModeView = () => {
1226
+ const mode = collectionMode ? collectionMode.value : (state.collectionMode || 'query');
1227
+ if (collectionIo && collectionIo.dataset) {
1228
+ collectionIo.dataset.mode = mode;
1229
+ }
1230
+ document.querySelectorAll('[data-collection-mode]').forEach((panel) => {
1231
+ const panelMode = panel.dataset ? panel.dataset.collectionMode : null;
1232
+ panel.classList.toggle('is-hidden', panelMode !== mode);
1233
+ });
1234
+ };
1235
+
1014
1236
  const renderCollections = (items) => {
1015
1237
  if (!collectionList) return;
1016
1238
  collectionList.innerHTML = '';
@@ -1093,6 +1315,7 @@
1093
1315
  if (collectionMode && entry.mode) {
1094
1316
  collectionMode.value = entry.mode;
1095
1317
  state.collectionMode = entry.mode;
1318
+ updateCollectionModeView();
1096
1319
  }
1097
1320
  if (collectionRunMode) {
1098
1321
  collectionRunMode.value = entry.runAsSystem ? 'system' : 'user';
@@ -1173,8 +1396,13 @@
1173
1396
  if (!keepPage) {
1174
1397
  state.collectionPage = 1;
1175
1398
  }
1399
+ const shouldRefreshTotals = !keepPage || !state.collectionTotal;
1176
1400
  state.collectionHasMore = false;
1177
- state.collectionTotal = 0;
1401
+ if (shouldRefreshTotals) {
1402
+ state.collectionTotal = 0;
1403
+ }
1404
+ state.collectionLoading = true;
1405
+ state.collectionTotalsLoading = shouldRefreshTotals;
1178
1406
  updateCollectionPager();
1179
1407
  const runAsSystem = !collectionRunMode || collectionRunMode.value === 'system';
1180
1408
  const selectedUser = state.selectedCollectionUser;
@@ -1208,10 +1436,15 @@
1208
1436
  if (typeof data.page === 'number') {
1209
1437
  state.collectionPage = data.page;
1210
1438
  }
1211
- if (typeof data.total === 'number') {
1212
- state.collectionTotal = data.total;
1213
- } else if (typeof data.count === 'number') {
1214
- state.collectionTotal = data.count;
1439
+ if (shouldRefreshTotals) {
1440
+ if (typeof data.total === 'number') {
1441
+ state.collectionTotal = data.total;
1442
+ } else if (typeof data.count === 'number') {
1443
+ state.collectionTotal = data.count;
1444
+ }
1445
+ if (typeof data.pageSize === 'number') {
1446
+ state.collectionPageSize = data.pageSize;
1447
+ }
1215
1448
  }
1216
1449
  updateCollectionPager();
1217
1450
  setCollectionTab('query');
@@ -1235,10 +1468,15 @@
1235
1468
  if (typeof data.page === 'number') {
1236
1469
  state.collectionPage = data.page;
1237
1470
  }
1238
- if (typeof data.total === 'number') {
1239
- state.collectionTotal = data.total;
1240
- } else if (typeof data.count === 'number') {
1241
- state.collectionTotal = data.count;
1471
+ if (shouldRefreshTotals) {
1472
+ if (typeof data.total === 'number') {
1473
+ state.collectionTotal = data.total;
1474
+ } else if (typeof data.count === 'number') {
1475
+ state.collectionTotal = data.count;
1476
+ }
1477
+ if (typeof data.pageSize === 'number') {
1478
+ state.collectionPageSize = data.pageSize;
1479
+ }
1242
1480
  }
1243
1481
  updateCollectionPager();
1244
1482
  setCollectionTab('query');
@@ -1258,6 +1496,9 @@
1258
1496
  setCollectionResult('Error: ' + err.message, false);
1259
1497
  }
1260
1498
  } finally {
1499
+ state.collectionLoading = false;
1500
+ state.collectionTotalsLoading = false;
1501
+ updateCollectionPager();
1261
1502
  if (recordHistory) {
1262
1503
  loadCollectionHistory();
1263
1504
  }
@@ -1282,6 +1523,7 @@
1282
1523
  typeFilter.value = '';
1283
1524
  state.events = [];
1284
1525
  state.selectedEvent = null;
1526
+ eventDetail.classList.remove('json-highlight');
1285
1527
  eventDetail.textContent = 'select an event to inspect payload';
1286
1528
  if (eventFunctionButton) {
1287
1529
  eventFunctionButton.classList.add('is-hidden');
@@ -1292,6 +1534,9 @@
1292
1534
 
1293
1535
  refreshUsers.addEventListener('click', loadUsers);
1294
1536
  refreshFunctions.addEventListener('click', loadFunctions);
1537
+ if (refreshTriggers) {
1538
+ refreshTriggers.addEventListener('click', loadTriggers);
1539
+ }
1295
1540
  if (refreshCollections) {
1296
1541
  refreshCollections.addEventListener('click', loadCollections);
1297
1542
  }
@@ -1379,7 +1624,9 @@
1379
1624
  state.collectionMode = collectionMode.value;
1380
1625
  state.collectionPage = 1;
1381
1626
  state.collectionHasMore = false;
1627
+ state.collectionLoading = false;
1382
1628
  updateCollectionPager();
1629
+ updateCollectionModeView();
1383
1630
  });
1384
1631
  }
1385
1632
 
@@ -1418,6 +1665,36 @@
1418
1665
  });
1419
1666
  }
1420
1667
 
1668
+ if (triggerList) {
1669
+ triggerList.addEventListener('click', (event) => {
1670
+ const target = (event.target && event.target.closest)
1671
+ ? event.target.closest('.trigger-row')
1672
+ : null;
1673
+ if (!target) return;
1674
+ const name = target.dataset.name;
1675
+ const entry = (state.triggers || []).find((item) => {
1676
+ const content = item && item.content ? item.content : null;
1677
+ const triggerName = (content && content.name) || item.fileName || 'trigger';
1678
+ return triggerName === name;
1679
+ });
1680
+ if (!entry) return;
1681
+ showTriggerDetail(entry);
1682
+ });
1683
+ }
1684
+
1685
+ if (triggerFunctionButton) {
1686
+ triggerFunctionButton.addEventListener('click', () => {
1687
+ const fnName = triggerFunctionButton.dataset.functionName;
1688
+ if (!fnName) return;
1689
+ state.selectedFunction = fnName;
1690
+ setActiveTab('functions');
1691
+ renderFunctions(state.functions);
1692
+ loadFunctionCode(fnName);
1693
+ });
1694
+ }
1695
+
1696
+ updateCollectionModeView();
1697
+
1421
1698
  mergedUsers.addEventListener('click', async (event) => {
1422
1699
  const target = event.target;
1423
1700
  if (!target) return;
@@ -1427,6 +1704,10 @@
1427
1704
  if (!id) return;
1428
1705
  if (action === 'toggle') {
1429
1706
  const disabled = target.textContent === 'disable';
1707
+ if (disabled) {
1708
+ const ok = confirm('Disable user ' + id + '?');
1709
+ if (!ok) return;
1710
+ }
1430
1711
  await api('/users/' + id + '/status', {
1431
1712
  method: 'PATCH',
1432
1713
  body: JSON.stringify({ disabled })
@@ -1454,8 +1735,10 @@
1454
1735
  });
1455
1736
  const entry = state.mergedUserMap[id];
1456
1737
  if (entry) {
1457
- userDetail.textContent = JSON.stringify(entry, null, 2);
1738
+ userDetail.classList.add('json-highlight');
1739
+ userDetail.innerHTML = highlightJson(JSON.stringify(entry, null, 2) || '');
1458
1740
  } else {
1741
+ userDetail.classList.remove('json-highlight');
1459
1742
  userDetail.textContent = 'User not found in cache';
1460
1743
  }
1461
1744
  });
@@ -1481,6 +1764,7 @@
1481
1764
  if (functionCode) {
1482
1765
  functionCode.addEventListener('input', () => {
1483
1766
  updateFunctionEditor();
1767
+ updateFunctionModifiedState();
1484
1768
  });
1485
1769
  functionCode.addEventListener('scroll', () => {
1486
1770
  syncFunctionEditorScroll();
@@ -1596,12 +1880,20 @@
1596
1880
  const target = (event.target && event.target.closest)
1597
1881
  ? event.target.closest('.function-row')
1598
1882
  : null;
1883
+ if (event.target && event.target.classList && event.target.classList.contains('trigger-link')) {
1884
+ const triggerName = event.target.dataset ? event.target.dataset.trigger : null;
1885
+ if (triggerName) {
1886
+ openTriggerByName(triggerName);
1887
+ }
1888
+ event.stopPropagation();
1889
+ return;
1890
+ }
1599
1891
  if (!target) return;
1600
1892
  const name = target.dataset.name;
1601
1893
  if (!name) return;
1602
1894
  state.selectedFunction = name;
1603
1895
  state.selectedHistoryIndex = null;
1604
- functionSelected.textContent = name;
1896
+ setFunctionSelectedLabel(name, false);
1605
1897
  functionResult.textContent = '';
1606
1898
  setRunModeForFunction(name);
1607
1899
  loadFunctionCode();
@@ -1677,7 +1969,7 @@
1677
1969
  if (!functionData) return;
1678
1970
  state.selectedFunction = functionData.name;
1679
1971
  state.selectedHistoryIndex = null;
1680
- functionSelected.textContent = functionData.name;
1972
+ setFunctionSelectedLabel(functionData.name, false);
1681
1973
  functionArgs.value = JSON.stringify(functionData.args || [], null, 2);
1682
1974
  functionResult.textContent = '';
1683
1975
  setRunModeForFunction(functionData.name);
@@ -1699,7 +1991,7 @@
1699
1991
  if (!entry) return;
1700
1992
  state.selectedFunction = entry.name;
1701
1993
  state.selectedHistoryIndex = index;
1702
- functionSelected.textContent = entry.name;
1994
+ setFunctionSelectedLabel(entry.name, false);
1703
1995
  functionArgs.value = JSON.stringify(entry.args || [], null, 2);
1704
1996
  if (functionRunMode && typeof entry.runAsSystem === 'boolean') {
1705
1997
  functionRunMode.value = entry.runAsSystem ? 'system' : 'user';
@@ -1720,7 +2012,14 @@
1720
2012
  setSelectedFunctionUser(null);
1721
2013
  }
1722
2014
  functionResult.textContent = '';
1723
- loadFunctionCode();
2015
+ const loadPromise = loadFunctionCode();
2016
+ if (entry.code) {
2017
+ Promise.resolve(loadPromise).then(() => {
2018
+ applyFunctionOverride(entry.code);
2019
+ });
2020
+ } else {
2021
+ updateFunctionModifiedState();
2022
+ }
1724
2023
  renderFunctions(state.functions);
1725
2024
  renderHistory();
1726
2025
  });
@@ -1785,6 +2084,7 @@
1785
2084
  setInterval(updateClock, 1000);
1786
2085
  updateClock();
1787
2086
  updateFunctionEditor();
2087
+ updateFunctionModifiedState();
1788
2088
  updateCollectionPager();
1789
2089
  setCollectionTab('query');
1790
2090
  setCollectionResultView('json');
@@ -1821,6 +2121,7 @@
1821
2121
  connectWs();
1822
2122
  loadUsers();
1823
2123
  loadFunctions();
2124
+ loadTriggers();
1824
2125
  loadCollections();
1825
2126
  loadCollectionHistory();
1826
2127
  })();