@mongoosejs/studio 0.3.6 → 0.3.7

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.
@@ -814,6 +814,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
814
814
  getTaskOverview: function getTaskOverview(params) {
815
815
  return client.post('', { action: 'Task.getTaskOverview', ...params }).then(res => res.data);
816
816
  },
817
+ getTasksOverTime: function getTasksOverTime(params) {
818
+ return client.post('', { action: 'Task.getTasksOverTime', ...params }).then(res => res.data);
819
+ },
817
820
  rescheduleTask: function rescheduleTask(params) {
818
821
  return client.post('', { action: 'Task.rescheduleTask', ...params }).then(res => res.data);
819
822
  },
@@ -1154,6 +1157,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
1154
1157
  getTaskOverview: function getTaskOverview(params) {
1155
1158
  return client.post('/Task/getTaskOverview', params).then(res => res.data);
1156
1159
  },
1160
+ getTasksOverTime: function getTasksOverTime(params) {
1161
+ return client.post('/Task/getTasksOverTime', params).then(res => res.data);
1162
+ },
1157
1163
  rescheduleTask: function rescheduleTask(params) {
1158
1164
  return client.post('/Task/rescheduleTask', params).then(res => res.data);
1159
1165
  },
@@ -2442,16 +2448,28 @@ module.exports = app => app.component('dashboard-primitive', {
2442
2448
  props: ['value'],
2443
2449
  computed: {
2444
2450
  header() {
2445
- if (this.value != null && this.value.$primitive.header) {
2451
+ if (this.value != null && this.value.$primitive?.header) {
2446
2452
  return this.value.$primitive.header;
2447
2453
  }
2448
2454
  return null;
2449
2455
  },
2450
2456
  displayValue() {
2451
2457
  if (this.value != null && this.value.$primitive) {
2458
+ if (this.value.$primitive.value === null) {
2459
+ return 'null';
2460
+ }
2452
2461
  return this.value.$primitive.value;
2453
2462
  }
2463
+ if (this.value === null) {
2464
+ return 'null';
2465
+ }
2454
2466
  return this.value;
2467
+ },
2468
+ displayClass() {
2469
+ if (this.value == null) {
2470
+ return 'text-content-tertiary';
2471
+ }
2472
+ return null;
2455
2473
  }
2456
2474
  }
2457
2475
  });
@@ -2520,6 +2538,7 @@ module.exports = app => app.component('dashboard-result', {
2520
2538
  (module, __unused_webpack_exports, __webpack_require__) {
2521
2539
 
2522
2540
  "use strict";
2541
+ /* global Blob, URL, document */
2523
2542
 
2524
2543
 
2525
2544
  const template = __webpack_require__(/*! ./dashboard-table.html */ "./frontend/src/dashboard-result/dashboard-table/dashboard-table.html");
@@ -2527,6 +2546,11 @@ const template = __webpack_require__(/*! ./dashboard-table.html */ "./frontend/s
2527
2546
  module.exports = app => app.component('dashboard-table', {
2528
2547
  template,
2529
2548
  props: ['value'],
2549
+ data() {
2550
+ return {
2551
+ showDropdown: false
2552
+ };
2553
+ },
2530
2554
  computed: {
2531
2555
  columns() {
2532
2556
  return Array.isArray(this.value?.$table?.columns) ? this.value.$table.columns : [];
@@ -2542,6 +2566,46 @@ module.exports = app => app.component('dashboard-table', {
2542
2566
  }
2543
2567
  },
2544
2568
  methods: {
2569
+ toggleDropdown() {
2570
+ this.showDropdown = !this.showDropdown;
2571
+ },
2572
+ handleBodyClick(event) {
2573
+ const dropdownRefs = this.$refs.dropdown;
2574
+ const dropdowns = Array.isArray(dropdownRefs) ? dropdownRefs : [dropdownRefs];
2575
+ const hasClickInsideDropdown = dropdowns
2576
+ .filter(dropdown => dropdown && typeof dropdown.contains === 'function')
2577
+ .some(dropdown => dropdown.contains(event.target));
2578
+
2579
+ if (!hasClickInsideDropdown) {
2580
+ this.showDropdown = false;
2581
+ }
2582
+ },
2583
+ neutralizeCsvCell(cell) {
2584
+ const value = this.displayValue(cell);
2585
+ return /^\s*[=+\-@]/.test(value) ? `'${value}` : value;
2586
+ },
2587
+ escapeCsvCell(cell) {
2588
+ const escapedCell = this.neutralizeCsvCell(cell).replaceAll('"', '""');
2589
+ return `"${escapedCell}"`;
2590
+ },
2591
+ downloadCsv() {
2592
+ const header = this.columns.map(this.escapeCsvCell).join(',');
2593
+ const rows = this.rows
2594
+ .map(row => row.map(this.escapeCsvCell).join(','))
2595
+ .join('\n');
2596
+
2597
+ const csv = [header, rows].filter(v => v.length > 0).join('\n');
2598
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
2599
+ const url = URL.createObjectURL(blob);
2600
+ const anchor = document.createElement('a');
2601
+ anchor.href = url;
2602
+ anchor.download = 'table.csv';
2603
+ document.body.appendChild(anchor);
2604
+ anchor.click();
2605
+ document.body.removeChild(anchor);
2606
+ URL.revokeObjectURL(url);
2607
+ this.$toast.success('CSV downloaded!');
2608
+ },
2545
2609
  displayValue(cell) {
2546
2610
  if (cell == null) {
2547
2611
  return '';
@@ -2555,6 +2619,12 @@ module.exports = app => app.component('dashboard-table', {
2555
2619
  }
2556
2620
  return String(cell);
2557
2621
  }
2622
+ },
2623
+ mounted() {
2624
+ document.body.addEventListener('click', this.handleBodyClick);
2625
+ },
2626
+ unmounted() {
2627
+ document.body.removeEventListener('click', this.handleBodyClick);
2558
2628
  }
2559
2629
  });
2560
2630
 
@@ -2940,6 +3010,140 @@ module.exports = app => app.component('detail-array', {
2940
3010
  }
2941
3011
  });
2942
3012
 
3013
+ /***/ },
3014
+
3015
+ /***/ "./frontend/src/detail-date/detail-date.js"
3016
+ /*!*************************************************!*\
3017
+ !*** ./frontend/src/detail-date/detail-date.js ***!
3018
+ \*************************************************/
3019
+ (module, __unused_webpack_exports, __webpack_require__) {
3020
+
3021
+ "use strict";
3022
+
3023
+
3024
+ const template = __webpack_require__(/*! ./detail-date.html */ "./frontend/src/detail-date/detail-date.html");
3025
+
3026
+ module.exports = app => app.component('detail-date', {
3027
+ template,
3028
+ props: ['value', 'viewMode'],
3029
+ emits: ['updated'],
3030
+ watch: {
3031
+ displayValue: {
3032
+ immediate: true,
3033
+ handler(val) {
3034
+ this.$emit('updated', val);
3035
+ }
3036
+ }
3037
+ },
3038
+ computed: {
3039
+ format() {
3040
+ if (this.viewMode != null && typeof this.viewMode === 'object') {
3041
+ return this.viewMode.format;
3042
+ }
3043
+ return this.viewMode;
3044
+ },
3045
+ timezone() {
3046
+ if (this.viewMode != null && typeof this.viewMode === 'object') {
3047
+ return this.viewMode.timezone || '';
3048
+ }
3049
+ return '';
3050
+ },
3051
+ parsedDate() {
3052
+ if (this.value == null) {
3053
+ return null;
3054
+ }
3055
+ const date = new Date(this.value);
3056
+ return Number.isNaN(date.getTime()) ? null : date;
3057
+ },
3058
+ displayValue() {
3059
+ if (this.value == null) {
3060
+ return String(this.value);
3061
+ }
3062
+ if (!this.parsedDate) {
3063
+ return 'Invalid Date';
3064
+ }
3065
+ return this.formatDateForDisplay(this.parsedDate);
3066
+ }
3067
+ },
3068
+ methods: {
3069
+ formatDateForDisplay(date) {
3070
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
3071
+ return 'Invalid Date';
3072
+ }
3073
+
3074
+ if (this.format === 'utc_iso') {
3075
+ return date.toISOString();
3076
+ }
3077
+
3078
+ if (this.format === 'local_browser') {
3079
+ return date.toLocaleString();
3080
+ }
3081
+
3082
+ if (this.format === 'unix_ms') {
3083
+ return String(date.getTime());
3084
+ }
3085
+
3086
+ if (this.format === 'unix_seconds') {
3087
+ return String(Math.floor(date.getTime() / 1000));
3088
+ }
3089
+
3090
+ if (this.format === 'duration_relative') {
3091
+ return this.formatRelativeDuration(date);
3092
+ }
3093
+
3094
+ if (this.format === 'custom_tz') {
3095
+ return this.formatCustomTimezone(date);
3096
+ }
3097
+
3098
+ return date.toISOString();
3099
+ },
3100
+ formatRelativeDuration(date) {
3101
+ const diffMs = date.getTime() - Date.now();
3102
+ const absMs = Math.abs(diffMs);
3103
+ const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
3104
+ const units = [
3105
+ { unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
3106
+ { unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
3107
+ { unit: 'day', ms: 24 * 60 * 60 * 1000 },
3108
+ { unit: 'hour', ms: 60 * 60 * 1000 },
3109
+ { unit: 'minute', ms: 60 * 1000 },
3110
+ { unit: 'second', ms: 1000 }
3111
+ ];
3112
+
3113
+ for (const { unit, ms } of units) {
3114
+ if (absMs >= ms || unit === 'second') {
3115
+ const value = Math.round(diffMs / ms);
3116
+ return rtf.format(value, unit);
3117
+ }
3118
+ }
3119
+
3120
+ return 'now';
3121
+ },
3122
+ formatCustomTimezone(date) {
3123
+ const tz = (this.timezone || '').trim();
3124
+ if (!tz) {
3125
+ return `${date.toISOString()} (enter an IANA timezone)`;
3126
+ }
3127
+
3128
+ try {
3129
+ return new Intl.DateTimeFormat(undefined, {
3130
+ timeZone: tz,
3131
+ year: 'numeric',
3132
+ month: '2-digit',
3133
+ day: '2-digit',
3134
+ hour: '2-digit',
3135
+ minute: '2-digit',
3136
+ second: '2-digit',
3137
+ timeZoneName: 'short'
3138
+ }).format(date);
3139
+ } catch (err) {
3140
+ return `Invalid timezone: ${tz}`;
3141
+ }
3142
+ }
3143
+ }
3144
+ });
3145
+
3146
+
2943
3147
  /***/ },
2944
3148
 
2945
3149
  /***/ "./frontend/src/detail-default/detail-default.js"
@@ -4064,6 +4268,58 @@ module.exports = app => app.component('detail-default', {
4064
4268
  });
4065
4269
 
4066
4270
 
4271
+ /***/ },
4272
+
4273
+ /***/ "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js"
4274
+ /*!**************************************************************************************!*\
4275
+ !*** ./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js ***!
4276
+ \**************************************************************************************/
4277
+ (module, __unused_webpack_exports, __webpack_require__) {
4278
+
4279
+ "use strict";
4280
+
4281
+
4282
+ const template = __webpack_require__(/*! ./date-view-mode-picker.html */ "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html");
4283
+
4284
+ module.exports = app => app.component('date-view-mode-picker', {
4285
+ template,
4286
+ props: ['viewMode', 'path'],
4287
+ emits: ['update:viewMode'],
4288
+ computed: {
4289
+ format() {
4290
+ if (this.viewMode != null && typeof this.viewMode === 'object') {
4291
+ return this.viewMode.format;
4292
+ }
4293
+ return this.viewMode;
4294
+ },
4295
+ timezone() {
4296
+ if (this.viewMode != null && typeof this.viewMode === 'object') {
4297
+ return this.viewMode.timezone || '';
4298
+ }
4299
+ return '';
4300
+ },
4301
+ timezoneDatalistId() {
4302
+ return `timezone-options-${String(this.path?.path || '').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
4303
+ },
4304
+ timezones() {
4305
+ return Intl.supportedValuesOf('timeZone');
4306
+ }
4307
+ },
4308
+ methods: {
4309
+ onFormatChange(newFormat) {
4310
+ if (newFormat === 'custom_tz') {
4311
+ this.$emit('update:viewMode', { format: 'custom_tz', timezone: this.timezone });
4312
+ } else {
4313
+ this.$emit('update:viewMode', newFormat);
4314
+ }
4315
+ },
4316
+ onTimezoneChange(newTimezone) {
4317
+ this.$emit('update:viewMode', { format: 'custom_tz', timezone: newTimezone });
4318
+ }
4319
+ }
4320
+ });
4321
+
4322
+
4067
4323
  /***/ },
4068
4324
 
4069
4325
  /***/ "./frontend/src/document-details/document-details.js"
@@ -4528,11 +4784,15 @@ const appendCSS = __webpack_require__(/*! ../../appendCSS */ "./frontend/src/app
4528
4784
 
4529
4785
  appendCSS(__webpack_require__(/*! ./document-property.css */ "./frontend/src/document-details/document-property/document-property.css"));
4530
4786
 
4787
+ const UNSET = Symbol('unset');
4788
+
4531
4789
  module.exports = app => app.component('document-property', {
4532
4790
  template,
4533
4791
  data: function() {
4534
4792
  return {
4535
4793
  dateType: 'picker', // picker, iso
4794
+ dateViewMode: 'utc_iso',
4795
+ renderedValue: UNSET,
4536
4796
  isCollapsed: false, // Start uncollapsed by default
4537
4797
  isValueExpanded: false, // Track if the value is expanded
4538
4798
  detailViewMode: 'text',
@@ -4549,8 +4809,14 @@ module.exports = app => app.component('document-property', {
4549
4809
  },
4550
4810
  props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid', 'highlight'],
4551
4811
  computed: {
4812
+ isDatePath() {
4813
+ return this.path?.instance === 'Date';
4814
+ },
4815
+ rawValue() {
4816
+ return this.getValueForPath(this.path.path);
4817
+ },
4552
4818
  valueAsString() {
4553
- const value = this.getValueForPath(this.path.path);
4819
+ const value = this.renderedValue !== UNSET ? this.renderedValue : this.rawValue;
4554
4820
  if (value == null) {
4555
4821
  return String(value);
4556
4822
  }
@@ -4682,6 +4948,9 @@ module.exports = app => app.component('document-property', {
4682
4948
  if (schemaPath.instance === 'Array') {
4683
4949
  return 'detail-array';
4684
4950
  }
4951
+ if (schemaPath.instance === 'Date') {
4952
+ return 'detail-date';
4953
+ }
4685
4954
  return 'detail-default';
4686
4955
  },
4687
4956
  getEditComponentForPath(path) {
@@ -9751,6 +10020,39 @@ const template = __webpack_require__(/*! ./tasks.html */ "./frontend/src/tasks/t
9751
10020
  const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
9752
10021
  const { DATE_FILTERS, getDateRangeForRange } = __webpack_require__(/*! ../_util/dateRange */ "./frontend/src/_util/dateRange.js");
9753
10022
 
10023
+ /** Returns the bucket size in ms for the given date range. */
10024
+ function getBucketSizeMs(range) {
10025
+ switch (range) {
10026
+ case 'last_hour': return 5 * 60 * 1000; // 5 minutes
10027
+ case 'today':
10028
+ case 'yesterday': return 60 * 60 * 1000; // 1 hour
10029
+ case 'thisWeek':
10030
+ case 'lastWeek': return 24 * 60 * 60 * 1000; // 1 day
10031
+ case 'thisMonth':
10032
+ case 'lastMonth': return 24 * 60 * 60 * 1000; // 1 day
10033
+ default: return 5 * 60 * 1000;
10034
+ }
10035
+ }
10036
+
10037
+ /** Formats a bucket timestamp for the x-axis label based on the date range. */
10038
+ function formatBucketLabel(timestamp, range) {
10039
+ const date = new Date(timestamp);
10040
+ switch (range) {
10041
+ case 'last_hour':
10042
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
10043
+ case 'today':
10044
+ case 'yesterday':
10045
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
10046
+ case 'thisWeek':
10047
+ case 'lastWeek':
10048
+ case 'thisMonth':
10049
+ case 'lastMonth':
10050
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
10051
+ default:
10052
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
10053
+ }
10054
+ }
10055
+
9754
10056
  module.exports = app => app.component('tasks', {
9755
10057
  data: () => ({
9756
10058
  status: 'init',
@@ -9778,10 +10080,22 @@ module.exports = app => app.component('tasks', {
9778
10080
  scheduledAt: '',
9779
10081
  parameters: '',
9780
10082
  repeatInterval: ''
9781
- }
10083
+ },
10084
+ // Chart over time
10085
+ overTimeChart: null,
10086
+ overTimeBuckets: [],
10087
+ // Toggled with v-if on the canvas so Chart.js is torn down and remounted on
10088
+ // filter changes. Updating Chart.js in place during a big Vue re-render was
10089
+ // freezing the page (dropdowns unresponsive, chart stale).
10090
+ showOverTimeChart: true
9782
10091
  }),
9783
10092
  methods: {
9784
10093
  async getTasks() {
10094
+ // Hide chart canvas + teardown Chart.js immediately on filter changes
10095
+ // (see showOverTimeChart + v-if on the canvas in tasks.html).
10096
+ this.showOverTimeChart = false;
10097
+ this.destroyOverTimeChart();
10098
+
9785
10099
  const params = {};
9786
10100
  if (this.selectedStatus == 'all') {
9787
10101
  params.status = null;
@@ -9800,9 +10114,105 @@ module.exports = app => app.component('tasks', {
9800
10114
  params.name = this.searchQuery.trim();
9801
10115
  }
9802
10116
 
9803
- const { statusCounts, tasksByName } = await api.Task.getTaskOverview(params);
9804
- this.statusCounts = statusCounts || this.statusCounts;
9805
- this.tasksByName = tasksByName || [];
10117
+ const [overviewResult, overTimeResult] = await Promise.all([
10118
+ api.Task.getTaskOverview(params),
10119
+ api.Task.getTasksOverTime({
10120
+ start: params.start,
10121
+ end: params.end,
10122
+ bucketSizeMs: getBucketSizeMs(this.selectedRange)
10123
+ })
10124
+ ]);
10125
+
10126
+ this.statusCounts = overviewResult.statusCounts || this.statusCounts;
10127
+ this.tasksByName = overviewResult.tasksByName || [];
10128
+ this.overTimeBuckets = overTimeResult || [];
10129
+ if (this.overTimeBuckets.length === 0) {
10130
+ this.showOverTimeChart = false;
10131
+ this.destroyOverTimeChart();
10132
+ } else {
10133
+ this.showOverTimeChart = true;
10134
+ await this.$nextTick();
10135
+ this.renderOverTimeChart();
10136
+ }
10137
+ },
10138
+
10139
+ /** Build or update the stacked bar chart showing tasks over time. */
10140
+ renderOverTimeChart() {
10141
+ const Chart = typeof window !== 'undefined' && window.Chart;
10142
+ if (!Chart) {
10143
+ throw new Error('Chart.js not found');
10144
+ }
10145
+ const canvas = this.$refs.overTimeChart;
10146
+ if (!canvas || typeof canvas.getContext !== 'function') return;
10147
+
10148
+ const buckets = this.overTimeBuckets;
10149
+ const labels = buckets.map(b => formatBucketLabel(b.timestamp, this.selectedRange));
10150
+ const succeeded = buckets.map(b => b.succeeded || 0);
10151
+ const failed = buckets.map(b => b.failed || 0);
10152
+ const cancelled = buckets.map(b => b.cancelled || 0);
10153
+
10154
+ const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
10155
+ const tickColor = isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)';
10156
+ const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
10157
+
10158
+ const chartData = {
10159
+ labels,
10160
+ datasets: [
10161
+ { label: 'Succeeded', data: succeeded, backgroundColor: '#22c55e', stack: 'tasks' },
10162
+ { label: 'Failed', data: failed, backgroundColor: '#ef4444', stack: 'tasks' },
10163
+ { label: 'Cancelled', data: cancelled, backgroundColor: '#6b7280', stack: 'tasks' }
10164
+ ]
10165
+ };
10166
+
10167
+ if (this.overTimeChart) {
10168
+ try {
10169
+ this.overTimeChart.data.labels = labels;
10170
+ this.overTimeChart.data.datasets[0].data = succeeded;
10171
+ this.overTimeChart.data.datasets[1].data = failed;
10172
+ this.overTimeChart.data.datasets[2].data = cancelled;
10173
+ this.overTimeChart.update('none');
10174
+ } finally {
10175
+ this.destroyOverTimeChart();
10176
+ }
10177
+ }
10178
+
10179
+ this.overTimeChart = new Chart(canvas, {
10180
+ type: 'bar',
10181
+ data: chartData,
10182
+ options: {
10183
+ responsive: true,
10184
+ maintainAspectRatio: false,
10185
+ animation: false,
10186
+ scales: {
10187
+ x: {
10188
+ stacked: true,
10189
+ ticks: { color: tickColor, maxRotation: 45, minRotation: 0 },
10190
+ grid: { color: gridColor }
10191
+ },
10192
+ y: {
10193
+ stacked: true,
10194
+ beginAtZero: true,
10195
+ ticks: { color: tickColor, precision: 0 },
10196
+ grid: { color: gridColor }
10197
+ }
10198
+ },
10199
+ plugins: {
10200
+ legend: {
10201
+ display: true,
10202
+ position: 'top',
10203
+ labels: { color: tickColor }
10204
+ },
10205
+ tooltip: { mode: 'index', intersect: false }
10206
+ }
10207
+ }
10208
+ });
10209
+ },
10210
+
10211
+ destroyOverTimeChart() {
10212
+ if (this.overTimeChart) {
10213
+ this.overTimeChart.destroy();
10214
+ this.overTimeChart = null;
10215
+ }
9806
10216
  },
9807
10217
  openTaskGroupDetails(group) {
9808
10218
  const query = { dateRange: this.selectedRange || 'last_hour' };
@@ -9978,11 +10388,23 @@ module.exports = app => app.component('tasks', {
9978
10388
  }
9979
10389
  },
9980
10390
  mounted: async function() {
10391
+ // Load initial data while showing the loader state.
9981
10392
  await this.updateDateRange();
9982
- await this.getTasks();
10393
+
10394
+ // Once data is loaded, switch to the main view.
9983
10395
  this.status = 'loaded';
10396
+ await this.$nextTick();
10397
+
10398
+ // Ensure the chart renders now that the canvas exists in the DOM.
10399
+ if (this.showOverTimeChart && this.overTimeBuckets.length > 0) {
10400
+ this.renderOverTimeChart();
10401
+ }
10402
+
9984
10403
  this.setDefaultCreateTaskValues();
9985
10404
  },
10405
+ beforeUnmount() {
10406
+ this.destroyOverTimeChart();
10407
+ },
9986
10408
  template: template
9987
10409
  });
9988
10410
 
@@ -10285,9 +10707,15 @@ var map = {
10285
10707
  "./detail-array/detail-array": "./frontend/src/detail-array/detail-array.js",
10286
10708
  "./detail-array/detail-array.html": "./frontend/src/detail-array/detail-array.html",
10287
10709
  "./detail-array/detail-array.js": "./frontend/src/detail-array/detail-array.js",
10710
+ "./detail-date/detail-date": "./frontend/src/detail-date/detail-date.js",
10711
+ "./detail-date/detail-date.html": "./frontend/src/detail-date/detail-date.html",
10712
+ "./detail-date/detail-date.js": "./frontend/src/detail-date/detail-date.js",
10288
10713
  "./detail-default/detail-default": "./frontend/src/detail-default/detail-default.js",
10289
10714
  "./detail-default/detail-default.html": "./frontend/src/detail-default/detail-default.html",
10290
10715
  "./detail-default/detail-default.js": "./frontend/src/detail-default/detail-default.js",
10716
+ "./document-details/date-view-mode-picker/date-view-mode-picker": "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js",
10717
+ "./document-details/date-view-mode-picker/date-view-mode-picker.html": "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html",
10718
+ "./document-details/date-view-mode-picker/date-view-mode-picker.js": "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js",
10291
10719
  "./document-details/document-details": "./frontend/src/document-details/document-details.js",
10292
10720
  "./document-details/document-details.css": "./frontend/src/document-details/document-details.css",
10293
10721
  "./document-details/document-details.html": "./frontend/src/document-details/document-details.html",
@@ -10506,7 +10934,7 @@ __webpack_require__.r(__webpack_exports__);
10506
10934
  /* harmony export */ });
10507
10935
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
10508
10936
  /**
10509
- * @vue/reactivity v3.5.31
10937
+ * @vue/reactivity v3.5.32
10510
10938
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
10511
10939
  * @license MIT
10512
10940
  **/
@@ -12653,7 +13081,7 @@ __webpack_require__.r(__webpack_exports__);
12653
13081
  /* harmony import */ var _vue_reactivity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/reactivity */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
12654
13082
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
12655
13083
  /**
12656
- * @vue/runtime-core v3.5.31
13084
+ * @vue/runtime-core v3.5.32
12657
13085
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
12658
13086
  * @license MIT
12659
13087
  **/
@@ -13600,6 +14028,7 @@ function createPathGetter(ctx, path) {
13600
14028
  };
13601
14029
  }
13602
14030
 
14031
+ const pendingMounts = /* @__PURE__ */ new WeakMap();
13603
14032
  const TeleportEndKey = /* @__PURE__ */ Symbol("_vte");
13604
14033
  const isTeleport = (type) => type.__isTeleport;
13605
14034
  const isTeleportDisabled = (props) => props && (props.disabled || props.disabled === "");
@@ -13641,91 +14070,86 @@ const TeleportImpl = {
13641
14070
  o: { insert, querySelector, createText, createComment }
13642
14071
  } = internals;
13643
14072
  const disabled = isTeleportDisabled(n2.props);
13644
- let { shapeFlag, children, dynamicChildren } = n2;
14073
+ let { dynamicChildren } = n2;
13645
14074
  if ( true && isHmrUpdating) {
13646
14075
  optimized = false;
13647
14076
  dynamicChildren = null;
13648
14077
  }
14078
+ const mount = (vnode, container2, anchor2) => {
14079
+ if (vnode.shapeFlag & 16) {
14080
+ mountChildren(
14081
+ vnode.children,
14082
+ container2,
14083
+ anchor2,
14084
+ parentComponent,
14085
+ parentSuspense,
14086
+ namespace,
14087
+ slotScopeIds,
14088
+ optimized
14089
+ );
14090
+ }
14091
+ };
14092
+ const mountToTarget = (vnode = n2) => {
14093
+ const disabled2 = isTeleportDisabled(vnode.props);
14094
+ const target = vnode.target = resolveTarget(vnode.props, querySelector);
14095
+ const targetAnchor = prepareAnchor(target, vnode, createText, insert);
14096
+ if (target) {
14097
+ if (namespace !== "svg" && isTargetSVG(target)) {
14098
+ namespace = "svg";
14099
+ } else if (namespace !== "mathml" && isTargetMathML(target)) {
14100
+ namespace = "mathml";
14101
+ }
14102
+ if (parentComponent && parentComponent.isCE) {
14103
+ (parentComponent.ce._teleportTargets || (parentComponent.ce._teleportTargets = /* @__PURE__ */ new Set())).add(target);
14104
+ }
14105
+ if (!disabled2) {
14106
+ mount(vnode, target, targetAnchor);
14107
+ updateCssVars(vnode, false);
14108
+ }
14109
+ } else if ( true && !disabled2) {
14110
+ warn$1("Invalid Teleport target on mount:", target, `(${typeof target})`);
14111
+ }
14112
+ };
14113
+ const queuePendingMount = (vnode) => {
14114
+ const mountJob = () => {
14115
+ if (pendingMounts.get(vnode) !== mountJob) return;
14116
+ pendingMounts.delete(vnode);
14117
+ if (isTeleportDisabled(vnode.props)) {
14118
+ mount(vnode, container, vnode.anchor);
14119
+ updateCssVars(vnode, true);
14120
+ }
14121
+ mountToTarget(vnode);
14122
+ };
14123
+ pendingMounts.set(vnode, mountJob);
14124
+ queuePostRenderEffect(mountJob, parentSuspense);
14125
+ };
13649
14126
  if (n1 == null) {
13650
14127
  const placeholder = n2.el = true ? createComment("teleport start") : 0;
13651
14128
  const mainAnchor = n2.anchor = true ? createComment("teleport end") : 0;
13652
14129
  insert(placeholder, container, anchor);
13653
14130
  insert(mainAnchor, container, anchor);
13654
- const mount = (container2, anchor2) => {
13655
- if (shapeFlag & 16) {
13656
- mountChildren(
13657
- children,
13658
- container2,
13659
- anchor2,
13660
- parentComponent,
13661
- parentSuspense,
13662
- namespace,
13663
- slotScopeIds,
13664
- optimized
13665
- );
13666
- }
13667
- };
13668
- const mountToTarget = () => {
13669
- const target = n2.target = resolveTarget(n2.props, querySelector);
13670
- const targetAnchor = prepareAnchor(target, n2, createText, insert);
13671
- if (target) {
13672
- if (namespace !== "svg" && isTargetSVG(target)) {
13673
- namespace = "svg";
13674
- } else if (namespace !== "mathml" && isTargetMathML(target)) {
13675
- namespace = "mathml";
13676
- }
13677
- if (parentComponent && parentComponent.isCE) {
13678
- (parentComponent.ce._teleportTargets || (parentComponent.ce._teleportTargets = /* @__PURE__ */ new Set())).add(target);
13679
- }
13680
- if (!disabled) {
13681
- mount(target, targetAnchor);
13682
- updateCssVars(n2, false);
13683
- }
13684
- } else if ( true && !disabled) {
13685
- warn$1(
13686
- "Invalid Teleport target on mount:",
13687
- target,
13688
- `(${typeof target})`
13689
- );
13690
- }
13691
- };
14131
+ if (isTeleportDeferred(n2.props) || parentSuspense && parentSuspense.pendingBranch) {
14132
+ queuePendingMount(n2);
14133
+ return;
14134
+ }
13692
14135
  if (disabled) {
13693
- mount(container, mainAnchor);
14136
+ mount(n2, container, mainAnchor);
13694
14137
  updateCssVars(n2, true);
13695
14138
  }
13696
- if (isTeleportDeferred(n2.props) || parentSuspense && parentSuspense.pendingBranch) {
13697
- n2.el.__isMounted = false;
13698
- queuePostRenderEffect(() => {
13699
- if (n2.el.__isMounted !== false) return;
13700
- mountToTarget();
13701
- delete n2.el.__isMounted;
13702
- }, parentSuspense);
13703
- } else {
13704
- mountToTarget();
13705
- }
14139
+ mountToTarget();
13706
14140
  } else {
13707
14141
  n2.el = n1.el;
13708
- n2.targetStart = n1.targetStart;
13709
14142
  const mainAnchor = n2.anchor = n1.anchor;
13710
- const target = n2.target = n1.target;
13711
- const targetAnchor = n2.targetAnchor = n1.targetAnchor;
13712
- if (n1.el.__isMounted === false) {
13713
- queuePostRenderEffect(() => {
13714
- TeleportImpl.process(
13715
- n1,
13716
- n2,
13717
- container,
13718
- anchor,
13719
- parentComponent,
13720
- parentSuspense,
13721
- namespace,
13722
- slotScopeIds,
13723
- optimized,
13724
- internals
13725
- );
13726
- }, parentSuspense);
14143
+ const pendingMount = pendingMounts.get(n1);
14144
+ if (pendingMount) {
14145
+ pendingMount.flags |= 8;
14146
+ pendingMounts.delete(n1);
14147
+ queuePendingMount(n2);
13727
14148
  return;
13728
14149
  }
14150
+ n2.targetStart = n1.targetStart;
14151
+ const target = n2.target = n1.target;
14152
+ const targetAnchor = n2.targetAnchor = n1.targetAnchor;
13729
14153
  const wasDisabled = isTeleportDisabled(n1.props);
13730
14154
  const currentContainer = wasDisabled ? container : target;
13731
14155
  const currentAnchor = wasDisabled ? mainAnchor : targetAnchor;
@@ -13816,13 +14240,19 @@ const TeleportImpl = {
13816
14240
  target,
13817
14241
  props
13818
14242
  } = vnode;
14243
+ let shouldRemove = doRemove || !isTeleportDisabled(props);
14244
+ const pendingMount = pendingMounts.get(vnode);
14245
+ if (pendingMount) {
14246
+ pendingMount.flags |= 8;
14247
+ pendingMounts.delete(vnode);
14248
+ shouldRemove = false;
14249
+ }
13819
14250
  if (target) {
13820
14251
  hostRemove(targetStart);
13821
14252
  hostRemove(targetAnchor);
13822
14253
  }
13823
14254
  doRemove && hostRemove(anchor);
13824
14255
  if (shapeFlag & 16) {
13825
- const shouldRemove = doRemove || !isTeleportDisabled(props);
13826
14256
  for (let i = 0; i < children.length; i++) {
13827
14257
  const child = children[i];
13828
14258
  unmount(
@@ -20031,6 +20461,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
20031
20461
  if (instance.isUnmounted || suspense.isUnmounted || suspense.pendingId !== instance.suspenseId) {
20032
20462
  return;
20033
20463
  }
20464
+ unsetCurrentInstance();
20034
20465
  instance.asyncResolved = true;
20035
20466
  const { vnode: vnode2 } = instance;
20036
20467
  if (true) {
@@ -21237,7 +21668,7 @@ function isMemoSame(cached, memo) {
21237
21668
  return true;
21238
21669
  }
21239
21670
 
21240
- const version = "3.5.31";
21671
+ const version = "3.5.32";
21241
21672
  const warn = true ? warn$1 : 0;
21242
21673
  const ErrorTypeStrings = ErrorTypeStrings$1 ;
21243
21674
  const devtools = true ? devtools$1 : 0;
@@ -21448,7 +21879,7 @@ __webpack_require__.r(__webpack_exports__);
21448
21879
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-core */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
21449
21880
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
21450
21881
  /**
21451
- * @vue/runtime-dom v3.5.31
21882
+ * @vue/runtime-dom v3.5.32
21452
21883
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
21453
21884
  * @license MIT
21454
21885
  **/
@@ -23520,7 +23951,7 @@ __webpack_require__.r(__webpack_exports__);
23520
23951
  /* harmony export */ toTypeString: () => (/* binding */ toTypeString)
23521
23952
  /* harmony export */ });
23522
23953
  /**
23523
- * @vue/shared v3.5.31
23954
+ * @vue/shared v3.5.32
23524
23955
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
23525
23956
  * @license MIT
23526
23957
  **/
@@ -49188,7 +49619,7 @@ __webpack_require__.r(__webpack_exports__);
49188
49619
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js");
49189
49620
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js");
49190
49621
  /**
49191
- * vue v3.5.31
49622
+ * vue v3.5.32
49192
49623
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
49193
49624
  * @license MIT
49194
49625
  **/
@@ -50474,7 +50905,7 @@ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b
50474
50905
  (module) {
50475
50906
 
50476
50907
  "use strict";
50477
- module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b border-gray-100 px-2 pb-2 text-xl font-bold\">\n {{header}}\n </div>\n <div class=\"text-xl p-2\">\n {{displayValue}}\n </div>\n</div>";
50908
+ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b border-gray-100 px-2 pb-2 text-xl font-bold\">\n {{header}}\n </div>\n <div class=\"text-xl p-2\" :class=\"displayClass\">\n {{displayValue}}\n </div>\n</div>\n";
50478
50909
 
50479
50910
  /***/ },
50480
50911
 
@@ -50485,7 +50916,7 @@ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b
50485
50916
  (module) {
50486
50917
 
50487
50918
  "use strict";
50488
- module.exports = "<div>\n <div v-if=\"Array.isArray(result)\">\n <div v-for=\"el in result\" :key=\"el._id || el.finishedEvaluatingAt\">\n <component\n class=\"bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(el)\"\n :value=\"el\">\n </component>\n </div>\n </div>\n <div v-else>\n <component\n class=\"bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(result)\"\n :value=\"result\"\n :fullscreen=\"fullscreen\"\n @fullscreen=\"$emit('fullscreen')\">\n </component>\n </div>\n <div class=\"text-right text-sm text-content-secondary mt-1\" v-if=\"finishedEvaluatingAt && !fullscreen\">\n Last Evaluated: {{ format.isoToLongDateTime(finishedEvaluatingAt) }}\n </div>\n</div>\n";
50919
+ module.exports = "<div>\n <div v-if=\"Array.isArray(result)\">\n <div v-for=\"(el, index) in result\">\n <component\n class=\"bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(el)\"\n :value=\"el\">\n </component>\n </div>\n </div>\n <div v-else>\n <component\n class=\"bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(result)\"\n :value=\"result\"\n :fullscreen=\"fullscreen\"\n @fullscreen=\"$emit('fullscreen')\">\n </component>\n </div>\n <div class=\"text-right text-sm text-content-secondary mt-1\" v-if=\"finishedEvaluatingAt && !fullscreen\">\n Last Evaluated: {{ format.isoToLongDateTime(finishedEvaluatingAt) }}\n </div>\n</div>\n";
50489
50920
 
50490
50921
  /***/ },
50491
50922
 
@@ -50496,7 +50927,7 @@ module.exports = "<div>\n <div v-if=\"Array.isArray(result)\">\n <div v-for=
50496
50927
  (module) {
50497
50928
 
50498
50929
  "use strict";
50499
- module.exports = "<div class=\"overflow-x-auto\">\n <table class=\"min-w-full border-separate border-spacing-0\">\n <thead v-if=\"hasColumns\" class=\"bg-slate-50\">\n <tr>\n <th\n v-for=\"(column, index) in columns\"\n :key=\"'column-' + index\"\n class=\"bg-slate-50 p-3 text-left text-sm font-semibold text-content border-b border-edge\"\n >\n {{ column }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(row, rowIndex) in rows\" :key=\"'row-' + rowIndex\" class=\"bg-surface hover:bg-slate-50\">\n <td\n v-for=\"(cell, columnIndex) in row\"\n :key=\"'cell-' + rowIndex + '-' + columnIndex\"\n class=\"p-3 text-sm text-content border-b border-edge\"\n >\n {{ displayValue(cell) }}\n </td>\n </tr>\n <tr v-if=\"!hasRows\">\n <td\n :colspan=\"Math.max(columns.length, 1)\"\n class=\"p-3 text-sm text-content-tertiary border-b border-edge\"\n >\n No rows\n </td>\n </tr>\n </tbody>\n </table>\n</div>\n";
50930
+ module.exports = "<div class=\"overflow-x-auto\">\n <table class=\"min-w-full border-separate border-spacing-0\">\n <thead v-if=\"hasColumns\" class=\"bg-slate-50\">\n <tr>\n <th\n v-for=\"(column, index) in columns\"\n :key=\"'column-' + index\"\n class=\"bg-slate-50 p-3 text-left text-sm font-semibold text-content border-b border-edge\"\n >\n <div v-if=\"index === columns.length - 1\" class=\"relative flex items-center gap-2 w-full\" ref=\"dropdown\">\n <span class=\"min-w-0 flex-1\">{{ column }}</span>\n <button\n @click.stop=\"toggleDropdown\"\n class=\"ml-auto rounded p-1 text-content-secondary hover:bg-muted hover:text-content\"\n aria-label=\"Table actions\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path d=\"M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z\" />\n </svg>\n </button>\n <div\n v-if=\"showDropdown\"\n class=\"absolute right-0 top-full z-10 mt-2 w-56 origin-top-right rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5\">\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-content-secondary hover:bg-muted\"\n @click=\"downloadCsv(); showDropdown = false\">\n Download as CSV\n </button>\n </div>\n </div>\n <template v-else>{{ column }}</template>\n </th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(row, rowIndex) in rows\" :key=\"'row-' + rowIndex\" class=\"bg-surface hover:bg-slate-50\">\n <td\n v-for=\"(cell, columnIndex) in row\"\n :key=\"'cell-' + rowIndex + '-' + columnIndex\"\n class=\"p-3 text-sm text-content border-b border-edge\"\n >\n {{ displayValue(cell) }}\n </td>\n </tr>\n <tr v-if=\"!hasRows\">\n <td\n :colspan=\"Math.max(columns.length, 1)\"\n class=\"p-3 text-sm text-content-tertiary border-b border-edge\"\n >\n No rows\n </td>\n </tr>\n </tbody>\n </table>\n</div>\n";
50500
50931
 
50501
50932
  /***/ },
50502
50933
 
@@ -50555,6 +50986,17 @@ module.exports = "<div class=\"w-full\">\n <div v-if=\"!arrayValue || arrayValu
50555
50986
 
50556
50987
  /***/ },
50557
50988
 
50989
+ /***/ "./frontend/src/detail-date/detail-date.html"
50990
+ /*!***************************************************!*\
50991
+ !*** ./frontend/src/detail-date/detail-date.html ***!
50992
+ \***************************************************/
50993
+ (module) {
50994
+
50995
+ "use strict";
50996
+ module.exports = "<pre class=\"w-full whitespace-pre-wrap break-words font-mono text-sm text-content-secondary m-0\">{{displayValue}}</pre>\n";
50997
+
50998
+ /***/ },
50999
+
50558
51000
  /***/ "./frontend/src/detail-default/detail-default.html"
50559
51001
  /*!*********************************************************!*\
50560
51002
  !*** ./frontend/src/detail-default/detail-default.html ***!
@@ -50566,6 +51008,17 @@ module.exports = "<div class=\"w-full\">\n <pre v-if=\"!isGeoJsonGeometry || !m
50566
51008
 
50567
51009
  /***/ },
50568
51010
 
51011
+ /***/ "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html"
51012
+ /*!****************************************************************************************!*\
51013
+ !*** ./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html ***!
51014
+ \****************************************************************************************/
51015
+ (module) {
51016
+
51017
+ "use strict";
51018
+ module.exports = "<div class=\"flex items-center gap-2\" @click.stop>\n <select\n :value=\"format\"\n @input=\"onFormatChange($event.target.value)\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1\"\n >\n <option value=\"utc_iso\">UTC (ISO)</option>\n <option value=\"local_browser\">Local (Browser)</option>\n <option value=\"unix_ms\">Unix (ms)</option>\n <option value=\"unix_seconds\">Unix (seconds)</option>\n <option value=\"duration_relative\">Duration relative to now</option>\n <option value=\"custom_tz\">Custom TZ...</option>\n </select>\n <input\n v-if=\"format === 'custom_tz'\"\n :value=\"timezone\"\n @input=\"onTimezoneChange($event.target.value.trim())\"\n :list=\"timezoneDatalistId\"\n type=\"text\"\n placeholder=\"America/New_York\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1 w-40\"\n >\n <datalist v-if=\"format === 'custom_tz'\" :id=\"timezoneDatalistId\">\n <option v-for=\"tz in timezones\" :key=\"tz\" :value=\"tz\"></option>\n </datalist>\n</div>\n";
51019
+
51020
+ /***/ },
51021
+
50569
51022
  /***/ "./frontend/src/document-details/document-details.css"
50570
51023
  /*!************************************************************!*\
50571
51024
  !*** ./frontend/src/document-details/document-details.css ***!
@@ -50606,7 +51059,7 @@ module.exports = ".document-details {\n width: 100%;\n }\n \n .document-de
50606
51059
  (module) {
50607
51060
 
50608
51061
  "use strict";
50609
- module.exports = "<div class=\"border border-edge bg-surface rounded-lg mb-2\" style=\"overflow: visible;\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-muted': !highlight }\"\n style=\"overflow: visible; position: relative;\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-content-tertiary\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n <div v-if=\"isGeoJsonGeometry\" class=\"ml-3 inline-flex items-center gap-2\">\n <div class=\"inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'text' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('text')\">\n Text\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'map' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'map' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('map')\">\n Map\n </button>\n </div>\n <!-- Info icon with tooltip -->\n <div v-if=\"editting\" class=\"relative inline-block\" style=\"z-index: 10002;\" @mouseenter=\"showTooltip = true\" @mouseleave=\"showTooltip = false\" @click.stop>\n <svg\n ref=\"infoIcon\"\n class=\"w-6 h-6 text-gray-400 hover:text-gray-600 cursor-help\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n <div\n v-show=\"showTooltip\"\n ref=\"tooltip\"\n class=\"absolute left-full top-0 ml-2 w-64 p-3 text-white text-xs rounded-lg shadow-xl\"\n style=\"z-index: 99999; pointer-events: none; white-space: normal; position: fixed; background-color: #111827;\"\n :style=\"getTooltipStyle()\"\n >\n <div class=\"font-semibold mb-2\">Map Controls:</div>\n <div v-if=\"isGeoJsonPoint\" class=\"space-y-1\">\n <div>• Drag pin to move location</div>\n </div>\n <div v-else-if=\"isGeoJsonPolygon\" class=\"space-y-1\">\n <div>• Drag vertices to reshape polygon</div>\n <div v-if=\"isMultiPolygon\">• Right-click edge to add new vertex</div>\n <div>• Right-click vertex to delete</div>\n </div>\n <div class=\"absolute top-2 -left-1 w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-primary hover:bg-primary-hover text-primary-text px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <!-- Use detail-default with map editing for GeoJSON geometries -->\n <component\n v-if=\"isGeoJsonGeometry\"\n :is=\"getComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :view-mode=\"detailViewMode\"\n :on-change=\"handleInputChange\"\n >\n </component>\n <!-- Use standard edit components for other types -->\n <component\n v-else\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-content-tertiary italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-content-secondary whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"getValueForPath(path.path)\"\n :view-mode=\"detailViewMode\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"getValueForPath(path.path)\"\n :view-mode=\"detailViewMode\"></component>\n </div>\n </div>\n </div>\n</div>\n";
51062
+ module.exports = "<div class=\"border border-edge bg-surface rounded-lg mb-2\" style=\"overflow: visible;\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-muted': !highlight }\"\n style=\"overflow: visible; position: relative;\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-content-tertiary\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n <div v-if=\"isGeoJsonGeometry\" class=\"ml-3 inline-flex items-center gap-2\">\n <div class=\"inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'text' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('text')\">\n Text\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'map' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'map' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('map')\">\n Map\n </button>\n </div>\n <!-- Info icon with tooltip -->\n <div v-if=\"editting\" class=\"relative inline-block\" style=\"z-index: 10002;\" @mouseenter=\"showTooltip = true\" @mouseleave=\"showTooltip = false\" @click.stop>\n <svg\n ref=\"infoIcon\"\n class=\"w-6 h-6 text-gray-400 hover:text-gray-600 cursor-help\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n <div\n v-show=\"showTooltip\"\n ref=\"tooltip\"\n class=\"absolute left-full top-0 ml-2 w-64 p-3 text-white text-xs rounded-lg shadow-xl\"\n style=\"z-index: 99999; pointer-events: none; white-space: normal; position: fixed; background-color: #111827;\"\n :style=\"getTooltipStyle()\"\n >\n <div class=\"font-semibold mb-2\">Map Controls:</div>\n <div v-if=\"isGeoJsonPoint\" class=\"space-y-1\">\n <div>• Drag pin to move location</div>\n </div>\n <div v-else-if=\"isGeoJsonPolygon\" class=\"space-y-1\">\n <div>• Drag vertices to reshape polygon</div>\n <div v-if=\"isMultiPolygon\">• Right-click edge to add new vertex</div>\n <div>• Right-click vertex to delete</div>\n </div>\n <div class=\"absolute top-2 -left-1 w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <date-view-mode-picker\n v-if=\"isDatePath\"\n :viewMode=\"dateViewMode\"\n :path=\"path\"\n @update:viewMode=\"dateViewMode = $event\"\n ></date-view-mode-picker>\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-primary hover:bg-primary-hover text-primary-text px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <!-- Use detail-default with map editing for GeoJSON geometries -->\n <component\n v-if=\"isGeoJsonGeometry\"\n :is=\"getComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :viewMode=\"detailViewMode\"\n :on-change=\"handleInputChange\"\n >\n </component>\n <!-- Use standard edit components for other types -->\n <component\n v-else\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-content-tertiary italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-content-secondary whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"isDatePath ? dateViewMode : detailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"isDatePath ? dateViewMode : detailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n </div>\n </div>\n </div>\n</div>\n";
50610
51063
 
50611
51064
  /***/ },
50612
51065
 
@@ -51068,7 +51521,7 @@ module.exports = "";
51068
51521
  (module) {
51069
51522
 
51070
51523
  "use strict";
51071
- module.exports = "<div class=\"p-4 space-y-6\">\n <div>\n <h1 class=\"text-2xl font-bold text-content-secondary mb-4\">Task Overview</h1>\n <div v-if=\"status == 'init'\">\n <img src=\"images/loader.gif\" />\n </div>\n <!-- Task List -->\n <div class=\"bg-surface p-4 rounded-lg shadow\" v-if=\"status == 'loaded'\">\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Filter by Date:</label>\n <select v-model=\"selectedRange\" @change=\"updateDateRange\" class=\"border-edge-strong rounded-md shadow-sm w-full p-2\">\n <option v-for=\"option in dateFilters\" :key=\"option.value\" :value=\"option.value\">\n {{ option.label }}\n </option>\n </select>\n </div>\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Filter by Status:</label>\n <select v-model=\"selectedStatus\" @change=\"getTasks\" class=\"border-edge-strong rounded-md shadow-sm w-full p-2\">\n <option v-for=\"option in statusFilters\" :key=\"option.value\" :value=\"option.value\">\n {{ option.label }}\n </option>\n </select>\n </div>\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Search by Task Name:</label>\n <input \n v-model=\"searchQuery\" \n type=\"text\" \n @input=\"onSearchInput\"\n class=\"border-edge-strong rounded-md shadow-sm w-full p-2\"\n placeholder=\"Enter task name to search...\"\n >\n </div>\n <div class=\"mb-4\">\n <button\n @click=\"resetFilters\"\n class=\"w-full bg-gray-200 text-content-secondary hover:bg-gray-300 font-medium py-2 px-4 rounded-md transition\"\n >\n Reset Filters\n </button>\n </div>\n <div class=\"mb-6\">\n <button\n @click=\"openCreateTaskModal\"\n class=\"w-full bg-primary text-primary-text hover:bg-primary-hover font-medium py-2 px-4 rounded-md transition\"\n >\n Create New Task\n </button>\n </div>\n <!-- Summary Section -->\n <div class=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <button \n @click=\"setStatusFilter('pending')\"\n :class=\"getStatusColor('pending') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-yellow-200 hover:border-yellow-300'\"\n >\n <div class=\"text-sm\">Scheduled</div>\n <div class=\"text-2xl font-bold\">{{pendingCount}}</div>\n </button>\n <button \n @click=\"setStatusFilter('succeeded')\"\n :class=\"getStatusColor('succeeded') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-green-200 hover:border-green-300'\"\n >\n <div class=\"text-sm\">Completed</div>\n <div class=\"text-2xl font-bold\">{{succeededCount}}</div>\n </button>\n <button \n @click=\"setStatusFilter('failed')\"\n :class=\"getStatusColor('failed') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-red-200 hover:border-red-300'\"\n >\n <div class=\"text-sm\">Failed</div>\n <div class=\"text-2xl font-bold\">{{failedCount}}</div>\n </button>\n <button \n @click=\"setStatusFilter('cancelled')\"\n :class=\"getStatusColor('cancelled') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-edge hover:border-edge-strong'\"\n >\n <div class=\"text-sm\">Cancelled</div>\n <div class=\"text-2xl font-bold\">{{cancelledCount}}</div>\n </button>\n </div>\n \n <!-- Grouped Task List -->\n <div class=\"mt-6\">\n <h2 class=\"text-lg font-semibold text-content-secondary mb-4\">Tasks by Name</h2>\n <ul class=\"divide-y divide-gray-200\">\n <li v-for=\"group in tasksByName\" :key=\"group.name\" class=\"p-4 group hover:border hover:rounded-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200\">\n <div class=\"flex items-center justify-between mb-3 \">\n <div class=\"flex-1 cursor-pointer\" @click=\"openTaskGroupDetails(group)\">\n <div class=\"flex items-center gap-2\">\n <div class=\"font-medium text-lg group-hover:text-primary transition-colors\">{{ group.name }}</div>\n <svg class=\"w-4 h-4 text-gray-400 group-hover:text-primary transition-colors\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n </div>\n <div class=\"text-sm text-content-tertiary group-hover:text-content-secondary transition-colors\">Total: {{ group.totalCount }} tasks</div>\n <div class=\"text-xs text-primary opacity-0 group-hover:opacity-100 transition-opacity mt-1\">\n Click to view details\n </div>\n </div>\n <div class=\"text-sm text-content-tertiary\">\n Last run: {{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }}\n </div>\n </div>\n \n <!-- Status Counts -->\n <div class=\"grid grid-cols-2 sm:grid-cols-4 gap-2\">\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'pending')\"\n class=\"bg-yellow-50 border border-yellow-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-yellow-300\"\n >\n <div class=\"text-xs text-yellow-600 font-medium\">Pending</div>\n <div class=\"text-lg font-bold text-yellow-700\">{{ group.statusCounts.pending || 0 }}</div>\n </button>\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'succeeded')\"\n class=\"bg-green-50 border border-green-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-green-300\"\n >\n <div class=\"text-xs text-green-600 font-medium\">Succeeded</div>\n <div class=\"text-lg font-bold text-green-700\">{{ group.statusCounts.succeeded || 0 }}</div>\n </button>\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'failed')\"\n class=\"bg-red-50 border border-red-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-red-300\"\n >\n <div class=\"text-xs text-red-600 font-medium\">Failed</div>\n <div class=\"text-lg font-bold text-red-700\">{{ group.statusCounts.failed || 0 }}</div>\n </button>\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'cancelled')\"\n class=\"bg-page border border-edge rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-edge-strong\"\n >\n <div class=\"text-xs text-gray-600 font-medium\">Cancelled</div>\n <div class=\"text-lg font-bold text-content-secondary\">{{ group.statusCounts.cancelled || 0 }}</div>\n </button>\n </div>\n </li>\n </ul>\n </div>\n </div>\n </div>\n\n <!-- Create Task Modal -->\n <modal v-if=\"showCreateTaskModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"closeCreateTaskModal\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"space-y-4\">\n <h3 class=\"text-lg font-semibold text-content-secondary mb-4\">Create New Task</h3>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Task Name:</label>\n <input \n v-model=\"newTask.name\" \n type=\"text\" \n class=\"w-full border border-edge-strong rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder=\"Enter task name\"\n >\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Scheduled Time:</label>\n <input \n v-model=\"newTask.scheduledAt\" \n type=\"datetime-local\" \n class=\"w-full border border-edge-strong rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Parameters (JSON):</label>\n <ace-editor\n v-model=\"newTask.parameters\"\n mode=\"json\"\n :line-numbers=\"true\"\n class=\"min-h-[120px]\"\n />\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Repeat Interval (ms):</label>\n <input \n v-model=\"newTask.repeatInterval\" \n type=\"number\" \n min=\"0\"\n step=\"1000\"\n class=\"w-full border border-edge-strong rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder=\"0 for no repetition\"\n >\n <p class=\"text-xs text-content-tertiary mt-1\">Enter 0 or leave empty for no repetition. Use 1000 for 1 second, 60000 for 1 minute, etc.</p>\n </div>\n \n <div class=\"flex gap-2 pt-4\">\n <button \n @click=\"createTask\" \n class=\"flex-1 bg-primary text-primary-text px-4 py-2 rounded-md hover:bg-primary-hover\"\n >\n Create Task\n </button>\n <button \n @click=\"closeCreateTaskModal\" \n class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400\"\n >\n Cancel\n </button>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
51524
+ module.exports = "<div class=\"p-4 space-y-6\">\n <div>\n <h1 class=\"text-2xl font-bold text-content-secondary mb-4\">Task Overview</h1>\n <div v-if=\"status == 'init'\">\n <img src=\"images/loader.gif\" />\n </div>\n <!-- Task List -->\n <div class=\"bg-surface p-4 rounded-lg shadow\" v-if=\"status == 'loaded'\">\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Filter by Date:</label>\n <select v-model=\"selectedRange\" @change=\"updateDateRange\" class=\"border-edge-strong rounded-md shadow-sm w-full p-2\">\n <option v-for=\"option in dateFilters\" :key=\"option.value\" :value=\"option.value\">\n {{ option.label }}\n </option>\n </select>\n </div>\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Filter by Status:</label>\n <select v-model=\"selectedStatus\" @change=\"getTasks\" class=\"border-edge-strong rounded-md shadow-sm w-full p-2\">\n <option v-for=\"option in statusFilters\" :key=\"option.value\" :value=\"option.value\">\n {{ option.label }}\n </option>\n </select>\n </div>\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Search by Task Name:</label>\n <input \n v-model=\"searchQuery\" \n type=\"text\" \n @input=\"onSearchInput\"\n class=\"border-edge-strong rounded-md shadow-sm w-full p-2\"\n placeholder=\"Enter task name to search...\"\n >\n </div>\n <div class=\"mb-4\">\n <button\n @click=\"resetFilters\"\n class=\"w-full bg-gray-200 text-content-secondary hover:bg-gray-300 font-medium py-2 px-4 rounded-md transition\"\n >\n Reset Filters\n </button>\n </div>\n <div class=\"mb-6\">\n <button\n @click=\"openCreateTaskModal\"\n class=\"w-full bg-primary text-primary-text hover:bg-primary-hover font-medium py-2 px-4 rounded-md transition\"\n >\n Create New Task\n </button>\n </div>\n <!-- Summary Section -->\n <div class=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <button \n @click=\"setStatusFilter('pending')\"\n :class=\"getStatusColor('pending') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-yellow-200 hover:border-yellow-300'\"\n >\n <div class=\"text-sm\">Scheduled</div>\n <div class=\"text-2xl font-bold\">{{pendingCount}}</div>\n </button>\n <button \n @click=\"setStatusFilter('succeeded')\"\n :class=\"getStatusColor('succeeded') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-green-200 hover:border-green-300'\"\n >\n <div class=\"text-sm\">Completed</div>\n <div class=\"text-2xl font-bold\">{{succeededCount}}</div>\n </button>\n <button \n @click=\"setStatusFilter('failed')\"\n :class=\"getStatusColor('failed') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-red-200 hover:border-red-300'\"\n >\n <div class=\"text-sm\">Failed</div>\n <div class=\"text-2xl font-bold\">{{failedCount}}</div>\n </button>\n <button \n @click=\"setStatusFilter('cancelled')\"\n :class=\"getStatusColor('cancelled') + ' p-4 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 cursor-pointer border-2 border-edge hover:border-edge-strong'\"\n >\n <div class=\"text-sm\">Cancelled</div>\n <div class=\"text-2xl font-bold\">{{cancelledCount}}</div>\n </button>\n </div>\n\n <!-- Tasks Over Time Chart -->\n <!--\n Canvas is gated by showOverTimeChart (v-if) so Chart.js is destroyed\n and the DOM node is removed before each refresh. In-place Chart.js\n updates during Vue re-renders from filter changes could freeze the UI\n (dropdowns stuck, chart not updating). See tasks.js getTasks().\n -->\n <div class=\"mt-6\">\n <h2 class=\"text-lg font-semibold text-content-secondary mb-3\">Tasks Over Time</h2>\n <div class=\"bg-page border border-edge rounded-lg p-4\" style=\"height: 260px;\">\n <canvas v-if=\"showOverTimeChart && overTimeBuckets.length > 0\" ref=\"overTimeChart\" style=\"width:100%;height:100%;\"></canvas>\n <div v-else class=\"flex items-center justify-center h-full text-content-tertiary text-sm\">\n No task activity in the selected window\n </div>\n </div>\n </div>\n \n <!-- Grouped Task List -->\n <div class=\"mt-6\">\n <h2 class=\"text-lg font-semibold text-content-secondary mb-4\">Tasks by Name</h2>\n <div class=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n <div v-for=\"group in tasksByName\" :key=\"group.name\" class=\"border border-edge rounded-lg p-4 group hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200\">\n <div class=\"flex items-start justify-between mb-3\">\n <div class=\"flex-1 cursor-pointer min-w-0 mr-2\" @click=\"openTaskGroupDetails(group)\">\n <div class=\"flex items-center gap-1\">\n <div class=\"font-medium text-sm group-hover:text-primary transition-colors truncate\">{{ group.name }}</div>\n <svg class=\"w-3 h-3 text-gray-400 group-hover:text-primary transition-colors flex-shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n </div>\n <div class=\"text-xs text-content-tertiary\">Total: {{ group.totalCount }} tasks</div>\n </div>\n <div class=\"text-xs text-content-tertiary flex-shrink-0\">\n {{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }}\n </div>\n </div>\n \n <!-- Status Counts -->\n <div class=\"grid grid-cols-2 gap-1.5\">\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'pending')\"\n class=\"bg-yellow-50 border border-yellow-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-yellow-300\"\n >\n <div class=\"text-xs text-yellow-600 font-medium\">Pending</div>\n <div class=\"text-base font-bold text-yellow-700\">{{ group.statusCounts.pending || 0 }}</div>\n </button>\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'succeeded')\"\n class=\"bg-green-50 border border-green-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-green-300\"\n >\n <div class=\"text-xs text-green-600 font-medium\">Succeeded</div>\n <div class=\"text-base font-bold text-green-700\">{{ group.statusCounts.succeeded || 0 }}</div>\n </button>\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'failed')\"\n class=\"bg-red-50 border border-red-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-red-300\"\n >\n <div class=\"text-xs text-red-600 font-medium\">Failed</div>\n <div class=\"text-base font-bold text-red-700\">{{ group.statusCounts.failed || 0 }}</div>\n </button>\n <button \n @click.stop=\"openTaskGroupDetailsWithFilter(group, 'cancelled')\"\n class=\"bg-page border border-edge rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-edge-strong\"\n >\n <div class=\"text-xs text-gray-600 font-medium\">Cancelled</div>\n <div class=\"text-base font-bold text-content-secondary\">{{ group.statusCounts.cancelled || 0 }}</div>\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Create Task Modal -->\n <modal v-if=\"showCreateTaskModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"closeCreateTaskModal\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"space-y-4\">\n <h3 class=\"text-lg font-semibold text-content-secondary mb-4\">Create New Task</h3>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Task Name:</label>\n <input \n v-model=\"newTask.name\" \n type=\"text\" \n class=\"w-full border border-edge-strong rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder=\"Enter task name\"\n >\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Scheduled Time:</label>\n <input \n v-model=\"newTask.scheduledAt\" \n type=\"datetime-local\" \n class=\"w-full border border-edge-strong rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Parameters (JSON):</label>\n <ace-editor\n v-model=\"newTask.parameters\"\n mode=\"json\"\n :line-numbers=\"true\"\n class=\"min-h-[120px]\"\n />\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Repeat Interval (ms):</label>\n <input \n v-model=\"newTask.repeatInterval\" \n type=\"number\" \n min=\"0\"\n step=\"1000\"\n class=\"w-full border border-edge-strong rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder=\"0 for no repetition\"\n >\n <p class=\"text-xs text-content-tertiary mt-1\">Enter 0 or leave empty for no repetition. Use 1000 for 1 second, 60000 for 1 minute, etc.</p>\n </div>\n \n <div class=\"flex gap-2 pt-4\">\n <button \n @click=\"createTask\" \n class=\"flex-1 bg-primary text-primary-text px-4 py-2 rounded-md hover:bg-primary-hover\"\n >\n Create Task\n </button>\n <button \n @click=\"closeCreateTaskModal\" \n class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400\"\n >\n Cancel\n </button>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
51072
51525
 
51073
51526
  /***/ },
51074
51527
 
@@ -62382,7 +62835,7 @@ var src_default = VueToastificationPlugin;
62382
62835
  (module) {
62383
62836
 
62384
62837
  "use strict";
62385
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.3.6","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","time-commando":"1.0.1","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js"}}');
62838
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.3.7","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","time-commando":"1.0.1","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js"}}');
62386
62839
 
62387
62840
  /***/ }
62388
62841