@live-change/task-frontend 0.9.162 → 0.9.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,273 @@
1
+ {
2
+ "version": "6.0",
3
+ "nxVersion": "18.0.6",
4
+ "deps": {
5
+ "@codemirror/language": "6.10.1",
6
+ "@dotenvx/dotenvx": "0.27.0",
7
+ "@fortawesome/fontawesome-free": "^6.5.2",
8
+ "@live-change/access-control-frontend": "0.8.32",
9
+ "@live-change/access-control-service": "0.8.32",
10
+ "@live-change/backup-service": "0.8.32",
11
+ "@live-change/blog-frontend": "0.8.32",
12
+ "@live-change/blog-service": "0.8.32",
13
+ "@live-change/cli": "0.8.32",
14
+ "@live-change/content-frontend": "0.8.32",
15
+ "@live-change/content-service": "0.8.32",
16
+ "@live-change/dao": "0.8.32",
17
+ "@live-change/dao-vue3": "0.8.32",
18
+ "@live-change/dao-websocket": "0.8.32",
19
+ "@live-change/db-client": "0.8.32",
20
+ "@live-change/email-service": "0.8.32",
21
+ "@live-change/framework": "0.8.32",
22
+ "@live-change/frontend-auto-form": "0.8.32",
23
+ "@live-change/frontend-base": "0.8.32",
24
+ "@live-change/geoip-service": "0.8.32",
25
+ "@live-change/image-frontend": "0.8.32",
26
+ "@live-change/locale-settings-service": "0.8.32",
27
+ "@live-change/password-authentication-service": "0.8.32",
28
+ "@live-change/prosemirror-service": "0.8.32",
29
+ "@live-change/secret-code-service": "0.8.32",
30
+ "@live-change/secret-link-service": "0.8.32",
31
+ "@live-change/session-service": "0.8.32",
32
+ "@live-change/task-service": "0.8.32",
33
+ "@live-change/upload-frontend": "0.8.32",
34
+ "@live-change/url-frontend": "0.8.32",
35
+ "@live-change/url-service": "0.8.32",
36
+ "@live-change/user-frontend": "0.8.32",
37
+ "@live-change/user-identification-service": "0.8.32",
38
+ "@live-change/user-service": "0.8.32",
39
+ "@live-change/vote-service": "0.8.32",
40
+ "@live-change/vue3-components": "0.8.32",
41
+ "@live-change/vue3-ssr": "0.8.32",
42
+ "@live-change/wysiwyg-frontend": "0.8.32",
43
+ "@vueuse/core": "^10.9.0",
44
+ "codeceptjs-assert": "^0.0.5",
45
+ "compression": "^1.7.4",
46
+ "cross-env": "^7.0.3",
47
+ "get-port-sync": "1.0.1",
48
+ "pica": "^9.0.1",
49
+ "pretty-bytes": "^6.1.1",
50
+ "primeflex": "^3.3.1",
51
+ "primeicons": "^7.0.0",
52
+ "primevue": "^3.51.0",
53
+ "rollup-plugin-node-builtins": "^2.1.2",
54
+ "rollup-plugin-visualizer": "5.12.0",
55
+ "serialize-javascript": "^6.0.2",
56
+ "serve-static": "^1.15.0",
57
+ "v-shared-element": "3.1.1",
58
+ "vue": "^3.4.19",
59
+ "vue-i18n": "^9.10.1",
60
+ "vue-router": "^4.2.5",
61
+ "vue3-scroll-border": "0.1.6",
62
+ "@live-change/codeceptjs-helper": "0.8.32",
63
+ "codeceptjs": "^3.5.12",
64
+ "generate-password": "1.7.1",
65
+ "playwright": "^1.41.2",
66
+ "random-profile-generator": "^2.3.0",
67
+ "txtgen": "^3.0.6",
68
+ "webdriverio": "^8.31.1"
69
+ },
70
+ "pathMappings": {},
71
+ "nxJsonPlugins": [],
72
+ "fileMap": {
73
+ "projectFileMap": {},
74
+ "nonProjectFiles": [
75
+ {
76
+ "file": ".gitignore",
77
+ "hash": "16528976885761916170"
78
+ },
79
+ {
80
+ "file": "Dockerfile",
81
+ "hash": "2886431590462419165"
82
+ },
83
+ {
84
+ "file": "LICENSE",
85
+ "hash": "10221119794387200971"
86
+ },
87
+ {
88
+ "file": "data/GeoLite2-Country.mmdb",
89
+ "hash": "18418324532701607635"
90
+ },
91
+ {
92
+ "file": "dev.Dockerfile",
93
+ "hash": "6907505572289099967"
94
+ },
95
+ {
96
+ "file": "docker/app.initd.sh",
97
+ "hash": "5689466769238316025"
98
+ },
99
+ {
100
+ "file": "docker/build-and-upload.sh",
101
+ "hash": "9432915210479550333"
102
+ },
103
+ {
104
+ "file": "docker/build-docker-and-upload.sh",
105
+ "hash": "9817365185106032741"
106
+ },
107
+ {
108
+ "file": "docker/commit-new-version.sh",
109
+ "hash": "5160018103227051012"
110
+ },
111
+ {
112
+ "file": "docker/k8s-rsync-helper.sh",
113
+ "hash": "12840580596915123557"
114
+ },
115
+ {
116
+ "file": "docker/onlyDependencies.js",
117
+ "hash": "13135394580253028435"
118
+ },
119
+ {
120
+ "file": "docker/parse-args-and-config.sh",
121
+ "hash": "11641932740456140593"
122
+ },
123
+ {
124
+ "file": "docker/restore-backup-with-service.sh",
125
+ "hash": "10918919254951884109"
126
+ },
127
+ {
128
+ "file": "docker/restore-backup.sh",
129
+ "hash": "17157081294258412437"
130
+ },
131
+ {
132
+ "file": "docker/start-service.sh",
133
+ "hash": "13107251553503965386"
134
+ },
135
+ {
136
+ "file": "docker/upload-backup-and-restore.sh",
137
+ "hash": "16296194178343837935"
138
+ },
139
+ {
140
+ "file": "front/index.html",
141
+ "hash": "9934749794764720310"
142
+ },
143
+ {
144
+ "file": "front/locales/en.js",
145
+ "hash": "10652573724821059037"
146
+ },
147
+ {
148
+ "file": "front/locales/en.json",
149
+ "hash": "7756539891184290419"
150
+ },
151
+ {
152
+ "file": "front/public/favicon.ico",
153
+ "hash": "11176796980188620760"
154
+ },
155
+ {
156
+ "file": "front/public/images/empty-photo.svg",
157
+ "hash": "9393138513813369674"
158
+ },
159
+ {
160
+ "file": "front/public/images/empty-user-photo.svg",
161
+ "hash": "17548039285248668212"
162
+ },
163
+ {
164
+ "file": "front/public/images/logo.svg",
165
+ "hash": "5934164668360941413"
166
+ },
167
+ {
168
+ "file": "front/public/images/logo128.png",
169
+ "hash": "4924117807669085214"
170
+ },
171
+ {
172
+ "file": "front/src/App.vue",
173
+ "hash": "3737234980433726371"
174
+ },
175
+ {
176
+ "file": "front/src/Index.vue",
177
+ "hash": "15104691593361452519"
178
+ },
179
+ {
180
+ "file": "front/src/ShelterProgress.vue",
181
+ "hash": "10500323481502622407"
182
+ },
183
+ {
184
+ "file": "front/src/analytics/index.js",
185
+ "hash": "492811393913301668"
186
+ },
187
+ {
188
+ "file": "front/src/components/BuildShelterResult.vue",
189
+ "hash": "12307738355148781708"
190
+ },
191
+ {
192
+ "file": "front/src/components/MakePlanksResult.vue",
193
+ "hash": "15891030442450754519"
194
+ },
195
+ {
196
+ "file": "front/src/components/Task.vue",
197
+ "hash": "690040641751569935"
198
+ },
199
+ {
200
+ "file": "front/src/config.js",
201
+ "hash": "4208481022464725661"
202
+ },
203
+ {
204
+ "file": "front/src/entry-client.js",
205
+ "hash": "1218837904372604167"
206
+ },
207
+ {
208
+ "file": "front/src/entry-server.js",
209
+ "hash": "7377383936979315921"
210
+ },
211
+ {
212
+ "file": "front/src/router.js",
213
+ "hash": "15530460209801505227"
214
+ },
215
+ {
216
+ "file": "front/tsconfig.json",
217
+ "hash": "6166366661637344199"
218
+ },
219
+ {
220
+ "file": "front/tsconfig.node.json",
221
+ "hash": "4208030242441905014"
222
+ },
223
+ {
224
+ "file": "front/vite.config.ts",
225
+ "hash": "4804285485121244435"
226
+ },
227
+ {
228
+ "file": "front/vite.config.ts.timestamp-1717571300028-addad6453ee85.mjs",
229
+ "hash": "17152765440974688639"
230
+ },
231
+ {
232
+ "file": "index.js",
233
+ "hash": "15558335606886518311"
234
+ },
235
+ {
236
+ "file": "package.json",
237
+ "hash": "17822390086587439626"
238
+ },
239
+ {
240
+ "file": "server/app.config.js",
241
+ "hash": "3863281573320850472"
242
+ },
243
+ {
244
+ "file": "server/init.js",
245
+ "hash": "10527942683746245757"
246
+ },
247
+ {
248
+ "file": "server/page.documentType.js",
249
+ "hash": "4319267302010918354"
250
+ },
251
+ {
252
+ "file": "server/security.config.js",
253
+ "hash": "6483730663450187822"
254
+ },
255
+ {
256
+ "file": "server/services.list.js",
257
+ "hash": "4032859481033847252"
258
+ },
259
+ {
260
+ "file": "server/start.js",
261
+ "hash": "3387118482182040319"
262
+ },
263
+ {
264
+ "file": "server/testTasks.js",
265
+ "hash": "14976257887791558210"
266
+ },
267
+ {
268
+ "file": "start-dev-docker.sh",
269
+ "hash": "1356997101209839594"
270
+ }
271
+ ]
272
+ }
273
+ }
Binary file
@@ -0,0 +1,6 @@
1
+ {
2
+ "nodes": {},
3
+ "externalNodes": {},
4
+ "dependencies": {},
5
+ "version": "6.0"
6
+ }
@@ -0,0 +1,41 @@
1
+ <template>
2
+
3
+ <ul class="bg-surface-0 dark:bg-surface-900 p-0 m-0 list-none flex overflow-x-auto select-none">
4
+ <li v-for="entry in entries" :key="entry.label" class="flex-1 p-0 m-0">
5
+ <router-link
6
+ :to="entry.route ? { name: entry.route, params: { } } : null"
7
+ :target="entry.target"
8
+ v-ripple
9
+ class="cursor-pointer px-4 py-3 flex items-center justify-center
10
+ border-y-2 hover:border-b-primary-400
11
+ transition-colors transition-duration-150 p-ripple no-underline"
12
+ :class="[
13
+ route.name === entry.route
14
+ ? 'border-b-surface-400 text-surface-300 hover:border-b-surface-700 border-t-transparent'
15
+ : 'text-surface-700 dark:text-surface-100 border-transparent'
16
+ ]">
17
+ <i v-if="entry.icon" class="mr-2" :class="[ entry.icon ]"></i>
18
+ <span class="font-medium">{{ entry.label }}</span>
19
+ </router-link>
20
+ </li>
21
+ </ul>
22
+
23
+ </template>
24
+
25
+ <script setup>
26
+
27
+ import { useRoute } from 'vue-router'
28
+ const route = useRoute()
29
+
30
+ const entries = [
31
+ { label: 'Intervals', icon: 'pi pi-clock', route: 'cron:admin:intervals' },
32
+ { label: 'Schedules', icon: 'pi pi-calendar', route: 'cron:admin:schedules' },
33
+ { label: 'Tasks', icon: 'pi pi-tasks', route: 'task:admin' },
34
+ ]
35
+
36
+ </script>
37
+
38
+
39
+ <style scoped>
40
+
41
+ </style>
@@ -6,6 +6,14 @@
6
6
  <div class="ml-2">{{ intervalData.description || intervalData.id }}</div>
7
7
  </div>
8
8
  <div class="flex flex-row items-center gap-4">
9
+ <div class="text-sm">
10
+ <strong>Last</strong>
11
+ {{ lastRunDisplay }}
12
+ </div>
13
+ <div class="text-sm">
14
+ <strong>Next</strong>
15
+ {{ nextRunDisplay }}
16
+ </div>
9
17
  <div class="text-sm">
10
18
  Every {{ formatInterval(intervalData.interval) }}
11
19
  </div>
@@ -15,13 +23,25 @@
15
23
  text
16
24
  rounded
17
25
  />
26
+ <Button
27
+ icon="pi pi-trash"
28
+ @click="deleteInterval"
29
+ text
30
+ rounded
31
+ />
18
32
  </div>
19
33
  </div>
20
34
 
21
35
  <div v-if="isExpanded" class="mt-2 p-2 bg-surface-50 dark:bg-surface-800 rounded">
22
36
  <div class="grid grid-cols-1 gap-2 text-sm">
23
- <div><strong>Interval:</strong> {{ formatInterval(intervalData.interval) }} ({{ intervalData.interval }}ms)</div>
24
- <div v-if="intervalData.wait"><strong>Wait:</strong> {{ formatInterval(intervalData.wait) }} ({{ intervalData.wait }}ms)</div>
37
+ <div>
38
+ <strong>Interval:</strong>
39
+ {{ formatInterval(intervalData.interval) }} ({{ intervalData.interval }}ms)
40
+ </div>
41
+ <div v-if="intervalData.wait">
42
+ <strong>Wait:</strong>
43
+ {{ formatInterval(intervalData.wait) }} ({{ intervalData.wait }}ms)
44
+ </div>
25
45
  </div>
26
46
  <div v-if="intervalData.trigger" class="mt-2">
27
47
  <strong>Trigger:</strong>
@@ -30,7 +50,8 @@
30
50
  <div><strong>Service:</strong> {{ intervalData.trigger.service || 'any' }}</div>
31
51
  <div v-if="intervalData.trigger.properties">
32
52
  <strong>Properties:</strong>
33
- <pre class="text-xs bg-surface-100 dark:bg-surface-700 p-1 rounded mt-1">{{ JSON.stringify(intervalData.trigger.properties, null, 2) }}</pre>
53
+ <pre class="text-xs bg-surface-100 dark:bg-surface-700 p-1 rounded mt-1"
54
+ >{{ JSON.stringify(intervalData.trigger.properties, null, 2) }}</pre>
34
55
  </div>
35
56
  </div>
36
57
  </div>
@@ -52,14 +73,37 @@
52
73
  </div>
53
74
  </div>
54
75
  </div>
76
+
77
+ <div v-if="intervalInfoData" class="mt-2 grid grid-cols-1 gap-1 text-sm">
78
+ <div v-if="intervalInfoData.lastRun">
79
+ <strong>Last Run: </strong>
80
+ <span :title="lastRunAbsolute || undefined">{{ lastRunDisplay }}</span>
81
+ </div>
82
+ <div v-if="intervalInfoData.nextRun">
83
+ <strong>Next Run: </strong>
84
+ <span :title="nextRunAbsolute || undefined">{{ nextRunDisplay }}</span>
85
+ </div>
86
+ </div>
87
+
88
+ <div v-if="tasksData?.length" class="mt-2">
89
+ <strong>Last 5 Tasks:</strong>
90
+ <div class="ml-2 text-sm">
91
+ <div v-for="task in tasksData" :key="task.id">
92
+ <TaskAdminCard :task="task" class="mt-1" />
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- <pre>{{ JSON.stringify(intervalData, null, 2) }}</pre> -->
55
98
  </div>
56
99
  </div>
57
100
  </template>
58
101
 
59
102
  <script setup>
60
- import { ref, computed, watch } from 'vue'
103
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
61
104
  import Button from 'primevue/button'
62
105
  import { usePath, live } from '@live-change/vue3-ssr'
106
+ import { currentTime } from "@live-change/frontend-base"
63
107
 
64
108
  const props = defineProps({
65
109
  interval: {
@@ -70,22 +114,6 @@
70
114
 
71
115
  const intervalData = computed(() => props.interval)
72
116
  const isExpanded = ref(false)
73
- const runStateData = ref(null)
74
- const path = usePath()
75
-
76
- // Watch for interval changes and fetch run state
77
- watch(() => props.interval?.id, async (newId) => {
78
- if (newId) {
79
- try {
80
- // Get run state for this specific interval
81
- const runStatePath = path.cron_RunState.to(['cron_Interval', newId])
82
- runStateData.value = await live(runStatePath)
83
- } catch (error) {
84
- console.error('Error fetching run state:', error)
85
- runStateData.value = null
86
- }
87
- }
88
- }, { immediate: true })
89
117
 
90
118
  function formatInterval(ms) {
91
119
  if (!ms) return 'N/A'
@@ -101,6 +129,8 @@
101
129
  return `${seconds} second${seconds > 1 ? 's' : ''}`
102
130
  }
103
131
 
132
+ const runStateData = computed(() => intervalData.value?.runState)
133
+
104
134
  const runStateIcon = computed(() => {
105
135
  switch(runStateData.value?.state) {
106
136
  case 'running': return 'pi-play'
@@ -116,4 +146,71 @@
116
146
  default: return ''
117
147
  }
118
148
  })
149
+
150
+ const intervalInfoData = computed(() => intervalData.value?.info)
151
+
152
+
153
+ function formatAbsoluteMoment(timestamp) {
154
+ if (!timestamp) return ''
155
+ const date = new Date(timestamp)
156
+ if (Number.isNaN(date.getTime())) return ''
157
+ return date.toLocaleString()
158
+ }
159
+
160
+ function formatRelativeMoment(timestamp) {
161
+ if (!timestamp) return 'N/A'
162
+ const target = new Date(timestamp).getTime()
163
+ if (Number.isNaN(target)) return 'N/A'
164
+
165
+ const diff = target - currentTime.value
166
+ const absDiff = Math.abs(diff)
167
+ const dayMs = 24 * 60 * 60 * 1000
168
+
169
+ if (absDiff < dayMs) {
170
+ const hourMs = 60 * 60 * 1000
171
+ const minuteMs = 60 * 1000
172
+ const hours = Math.floor(absDiff / hourMs)
173
+ const minutes = Math.floor((absDiff % hourMs) / minuteMs)
174
+ const seconds = Math.floor((absDiff % minuteMs) / 1000)
175
+ const parts = []
176
+ if (hours > 0) parts.push(`${hours}h`)
177
+ if (minutes > 0) parts.push(`${minutes}m`)
178
+ parts.push(`${seconds}s`)
179
+ const relative = parts.join(' ')
180
+ if (diff > 0) return `in ${relative}`
181
+ if (diff < 0) return `${relative} ago`
182
+ return 'now'
183
+ }
184
+
185
+ return new Date(target).toLocaleString()
186
+ }
187
+
188
+ const lastRunDisplay = computed(() => formatRelativeMoment(intervalInfoData.value?.lastRun))
189
+ const nextRunDisplay = computed(() => formatRelativeMoment(intervalInfoData.value?.nextRun))
190
+ const lastRunAbsolute = computed(() => formatAbsoluteMoment(intervalInfoData.value?.lastRun))
191
+ const nextRunAbsolute = computed(() => formatAbsoluteMoment(intervalInfoData.value?.nextRun))
192
+
193
+ const tasksData = computed(() => intervalData.value?.tasks)
194
+
195
+ import { useConfirm } from 'primevue/useconfirm'
196
+ const confirm = useConfirm()
197
+ import { useToast } from 'primevue/usetoast'
198
+ const toast = useToast()
199
+
200
+ function deleteInterval() {
201
+ confirm.require({
202
+ target: event.currentTarget,
203
+ message: `Do you want to delete this interval?`,
204
+ icon: 'pi pi-info-circle',
205
+ acceptClass: 'p-button-danger',
206
+ accept: async () => {
207
+ console.log("deleteInterval", intervalData.value.id)
208
+ await api.actions.cron.deleteInterval({ interval: intervalData.value.id })
209
+ toast.add({ severity:'info', summary: 'Interval deleted', life: 1500 })
210
+ },
211
+ reject: () => {
212
+ toast.add({ severity:'error', summary: 'Rejected', detail: 'You have rejected', life: 3000 })
213
+ }
214
+ })
215
+ }
119
216
  </script>
@@ -2,10 +2,18 @@
2
2
  <div class="bg-surface-0 dark:bg-surface-900 px-3 py-1 shadow">
3
3
  <div class="flex flex-row justify-between items-center" v-if="scheduleData">
4
4
  <div class="flex flex-row items-center">
5
- <i class="pi pi-calendar" style="font-size: 1rem" />
5
+ <i class="pi pi-clock" style="font-size: 1rem" />
6
6
  <div class="ml-2">{{ scheduleData.description || scheduleData.id }}</div>
7
7
  </div>
8
8
  <div class="flex flex-row items-center gap-4">
9
+ <div class="text-sm">
10
+ <strong>Last</strong>
11
+ {{ lastRunDisplay }}
12
+ </div>
13
+ <div class="text-sm">
14
+ <strong>Next</strong>
15
+ {{ nextRunDisplay }}
16
+ </div>
9
17
  <div class="text-sm">
10
18
  {{ formatSchedule(scheduleData) }}
11
19
  </div>
@@ -15,16 +23,18 @@
15
23
  text
16
24
  rounded
17
25
  />
26
+ <Button
27
+ icon="pi pi-trash"
28
+ @click="deleteSchedule"
29
+ text
30
+ rounded
31
+ />
18
32
  </div>
19
33
  </div>
20
34
 
21
35
  <div v-if="isExpanded" class="mt-2 p-2 bg-surface-50 dark:bg-surface-800 rounded">
22
- <div class="grid grid-cols-2 gap-2 text-sm">
23
- <div><strong>Minute:</strong> {{ scheduleData.minute || '*' }}</div>
24
- <div><strong>Hour:</strong> {{ scheduleData.hour || '*' }}</div>
25
- <div><strong>Day:</strong> {{ scheduleData.day || '*' }}</div>
26
- <div><strong>Day of Week:</strong> {{ scheduleData.dayOfWeek || '*' }}</div>
27
- <div><strong>Month:</strong> {{ scheduleData.month || '*' }}</div>
36
+ <div class="grid grid-cols-1 gap-2 text-sm">
37
+ <div><strong>Schedule:</strong> {{ formatSchedule(scheduleData) }}</div>
28
38
  </div>
29
39
  <div v-if="scheduleData.trigger" class="mt-2">
30
40
  <strong>Trigger:</strong>
@@ -33,12 +43,14 @@
33
43
  <div><strong>Service:</strong> {{ scheduleData.trigger.service || 'any' }}</div>
34
44
  <div v-if="scheduleData.trigger.properties">
35
45
  <strong>Properties:</strong>
36
- <pre class="text-xs bg-surface-100 dark:bg-surface-700 p-1 rounded mt-1">{{ JSON.stringify(scheduleData.trigger.properties, null, 2) }}</pre>
46
+ <pre class="text-xs bg-surface-100 dark:bg-surface-700 p-1 rounded mt-1">
47
+ {{ JSON.stringify(scheduleData.trigger.properties, null, 2) }}
48
+ </pre>
37
49
  </div>
38
50
  </div>
39
51
  </div>
40
52
 
41
- <!-- RunState for this Schedule -->
53
+ <!-- RunState for this Interval -->
42
54
  <div v-if="runStateData" class="mt-2">
43
55
  <strong>Current Run State:</strong>
44
56
  <div class="ml-2 text-sm p-2 bg-surface-100 dark:bg-surface-700 rounded">
@@ -55,14 +67,37 @@
55
67
  </div>
56
68
  </div>
57
69
  </div>
70
+
71
+ <div v-if="scheduleInfoData" class="mt-2 grid grid-cols-1 gap-1 text-sm">
72
+ <div v-if="scheduleInfoData.lastRun">
73
+ <strong>Last Run: </strong>
74
+ <span :title="lastRunAbsolute || undefined">{{ lastRunDisplay }}</span>
75
+ </div>
76
+ <div v-if="scheduleInfoData.nextRun">
77
+ <strong>Next Run: </strong>
78
+ <span :title="nextRunAbsolute || undefined">{{ nextRunDisplay }}</span>
79
+ </div>
80
+ </div>
81
+
82
+ <div v-if="tasksData?.length" class="mt-2">
83
+ <strong>Last 5 Tasks:</strong>
84
+ <div class="ml-2 text-sm">
85
+ <div v-for="task in tasksData" :key="task.id">
86
+ <TaskAdminCard :task="task" class="mt-1" />
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <pre>{{ JSON.stringify(intervalData, null, 2) }}</pre>
58
92
  </div>
59
93
  </div>
60
94
  </template>
61
95
 
62
96
  <script setup>
63
- import { ref, computed, onMounted, watch } from 'vue'
97
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
64
98
  import Button from 'primevue/button'
65
99
  import { usePath, live } from '@live-change/vue3-ssr'
100
+ import { currentTime } from "@live-change/frontend-base"
66
101
 
67
102
  const props = defineProps({
68
103
  schedule: {
@@ -73,43 +108,23 @@
73
108
 
74
109
  const scheduleData = computed(() => props.schedule)
75
110
  const isExpanded = ref(false)
76
- const runStateData = ref(null)
77
- const path = usePath()
78
-
79
- // Watch for schedule changes and fetch run state
80
- watch(() => props.schedule?.id, async (newId) => {
81
- if (newId) {
82
- try {
83
- // Get the run state for this specific schedule
84
- const runStatePath = path.cron_RunState.to(['cron_Schedule', newId])
85
- runStateData.value = await live(runStatePath)
86
- } catch (error) {
87
- console.error('Error fetching run state:', error)
88
- runStateData.value = null
89
- }
90
- }
91
- }, { immediate: true })
111
+
112
+ const daysOfWeek = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
113
+ const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
92
114
 
93
115
  function formatSchedule(schedule) {
94
116
  const parts = []
95
- if (schedule.minute !== undefined && schedule.minute !== null) parts.push(schedule.minute)
96
- else parts.push('*')
97
-
98
- if (schedule.hour !== undefined && schedule.hour !== null) parts.push(schedule.hour)
99
- else parts.push('*')
100
-
101
- if (schedule.day !== undefined && schedule.day !== null) parts.push(schedule.day)
102
- else parts.push('*')
103
-
104
- if (schedule.month !== undefined && schedule.month !== null) parts.push(schedule.month)
105
- else parts.push('*')
106
-
107
- if (schedule.dayOfWeek !== undefined && schedule.dayOfWeek !== null) parts.push(schedule.dayOfWeek)
108
- else parts.push('*')
109
-
117
+ if(Number.isInteger(schedule.minute)) parts.push(`${schedule.minute}m`)
118
+ if(Number.isInteger(schedule.hour)) parts.push(`${schedule.hour}h`)
119
+ if(Number.isInteger(schedule.day)) parts.push(`${schedule.day}`)
120
+ if(Number.isInteger(schedule.dayOfWeek)) parts.push(`${daysOfWeek[schedule.dayOfWeek - 1]}`)
121
+ if(Number.isInteger(schedule.month)) parts.push(`${months[schedule.month - 1]}`)
122
+ if(parts.length === 0) parts.push('every minute')
110
123
  return parts.join(' ')
111
124
  }
112
125
 
126
+ const runStateData = computed(() => scheduleData.value?.runState)
127
+
113
128
  const runStateIcon = computed(() => {
114
129
  switch(runStateData.value?.state) {
115
130
  case 'running': return 'pi-play'
@@ -125,4 +140,71 @@
125
140
  default: return ''
126
141
  }
127
142
  })
143
+
144
+ const scheduleInfoData = computed(() => scheduleData.value?.info)
145
+
146
+
147
+ function formatAbsoluteMoment(timestamp) {
148
+ if (!timestamp) return ''
149
+ const date = new Date(timestamp)
150
+ if (Number.isNaN(date.getTime())) return ''
151
+ return date.toLocaleString()
152
+ }
153
+
154
+ function formatRelativeMoment(timestamp) {
155
+ if (!timestamp) return 'N/A'
156
+ const target = new Date(timestamp).getTime()
157
+ if (Number.isNaN(target)) return 'N/A'
158
+
159
+ const diff = target - currentTime.value
160
+ const absDiff = Math.abs(diff)
161
+ const dayMs = 24 * 60 * 60 * 1000
162
+
163
+ if (absDiff < dayMs) {
164
+ const hourMs = 60 * 60 * 1000
165
+ const minuteMs = 60 * 1000
166
+ const hours = Math.floor(absDiff / hourMs)
167
+ const minutes = Math.floor((absDiff % hourMs) / minuteMs)
168
+ const seconds = Math.floor((absDiff % minuteMs) / 1000)
169
+ const parts = []
170
+ if (hours > 0) parts.push(`${hours}h`)
171
+ if (minutes > 0) parts.push(`${minutes}m`)
172
+ parts.push(`${seconds}s`)
173
+ const relative = parts.join(' ')
174
+ if (diff > 0) return `in ${relative}`
175
+ if (diff < 0) return `${relative} ago`
176
+ return 'now'
177
+ }
178
+
179
+ return new Date(target).toLocaleString()
180
+ }
181
+
182
+ const lastRunDisplay = computed(() => formatRelativeMoment(scheduleInfoData.value?.lastRun))
183
+ const nextRunDisplay = computed(() => formatRelativeMoment(scheduleInfoData.value?.nextRun))
184
+ const lastRunAbsolute = computed(() => formatAbsoluteMoment(scheduleInfoData.value?.lastRun))
185
+ const nextRunAbsolute = computed(() => formatAbsoluteMoment(scheduleInfoData.value?.nextRun))
186
+
187
+ const tasksData = computed(() => scheduleData.value?.tasks)
188
+
189
+ import { useConfirm } from 'primevue/useconfirm'
190
+ const confirm = useConfirm()
191
+ import { useToast } from 'primevue/usetoast'
192
+ const toast = useToast()
193
+
194
+ function deleteSchedule() {
195
+ confirm.require({
196
+ target: event.currentTarget,
197
+ message: `Do you want to delete this schedule?`,
198
+ icon: 'pi pi-info-circle',
199
+ acceptClass: 'p-button-danger',
200
+ accept: async () => {
201
+ console.log("deleteSchedule", scheduleData.value.id)
202
+ await api.actions.cron.deleteSchedule({ schedule: scheduleData.value.id })
203
+ toast.add({ severity:'info', summary: 'Schedule deleted', life: 1500 })
204
+ },
205
+ reject: () => {
206
+ toast.add({ severity:'error', summary: 'Rejected', detail: 'You have rejected', life: 3000 })
207
+ }
208
+ })
209
+ }
128
210
  </script>
@@ -35,6 +35,7 @@
35
35
  {{ (new Date(taskData.doneAt).getTime() - new Date(taskData.startedAt).getTime()) / 1000 }}s
36
36
  </div>
37
37
  </div>
38
+
38
39
 
39
40
  <div class="grid grid-cols-2 gap-2 text-sm" v-if="taskData.doneAt">
40
41
  <div v-if="taskData.doneAt">Done:</div>
@@ -47,6 +48,11 @@
47
48
  {{ taskData.client }}
48
49
  </div>
49
50
  </div>
51
+
52
+ <div class="grid grid-cols-2 gap-2 text-sm" v-if="taskData.cause">
53
+ <div>Cause:</div>
54
+ <div>{{ taskData.causeType }} - {{ taskData.cause }}</div>
55
+ </div>
50
56
  </div>
51
57
 
52
58
  <div v-if="taskData.properties" class="mt-2">
@@ -1,83 +1,20 @@
1
1
  <template>
2
- <div class="w-full">
3
-
4
- <ActionForm
5
- service="cron"
6
- action="setInterval"
7
- :parameters="intervalParameters"
8
- @done="handleActionDone" />
9
-
10
-
11
- <div class="bg-surface-0 dark:bg-surface-900 p-3 shadow mb-1">
12
- <h3>{{ t('cron.intervals') }}</h3>
13
- <range-viewer :pathFunction="intervalsPathFunction" :key="JSON.stringify(intervalsPathConfig)"
14
- :canLoadTop="false" :canDropBottom="false"
15
- loadBottomSensorSize="3000px" dropBottomSensorSize="12000px">
16
- <template #empty>
17
- <div class="bg-surface-0 p-3 shadow text-center text-gray-500 text-lg">
18
- No intervals found...
19
- </div>
20
- </template>
21
-
22
- <template #default="{ item: interval }">
23
- <IntervalCard :interval="interval" class="mt-1" />
24
- </template>
25
- </range-viewer>
2
+ <div class="w-full px-4 py-5 flex flex-col items-center justify-center">
3
+ <div class="bg-surface-0 dark:bg-surface-900 p-3 shadow mb-1 w-60 text-center mb-4">
4
+ <h1 class="my-1 text-center">Cron Admin</h1>
26
5
  </div>
27
-
6
+ <div class="flex flex-row gap-4 items-center justify-center">
7
+ <router-link :to="{ name: 'cron:admin:intervals' }" class="no-underline">
8
+ <Button label="Intervals" icon="pi pi-clock" size="large" />
9
+ </router-link>
10
+ <router-link :to="{ name: 'cron:admin:schedules' }" class="no-underline">
11
+ <Button label="Schedules" icon="pi pi-calendar" size="large" />
12
+ </router-link>
13
+ </div>
14
+
28
15
  </div>
29
16
  </template>
30
17
 
31
18
  <script setup>
32
-
33
- import ScheduleCard from '../components/ScheduleCard.vue'
34
- import IntervalCard from '../components/IntervalCard.vue'
35
- import Select from 'primevue/select'
36
- import InputText from 'primevue/inputtext'
37
- import InputNumber from 'primevue/inputnumber'
38
- import Dropdown from 'primevue/dropdown'
39
- import Button from 'primevue/button'
40
-
41
-
42
- import { ActionForm } from '@live-change/frontend-auto-form'
43
-
44
- import { useI18n } from 'vue-i18n'
45
- const { t } = useI18n()
46
-
47
- import { ref, computed } from 'vue'
48
- import { RangeViewer } from "@live-change/vue3-components"
49
-
50
- import { inject } from 'vue'
51
- const workingZone = inject('workingZone')
52
-
53
- import { usePath, live, useClient, useActions, reverseRange, useApi } from '@live-change/vue3-ssr'
54
- const path = usePath()
55
- const client = useClient()
56
- const actions = useActions()
57
- const api = useApi()
58
-
59
- const taskType = ref('all')
60
-
61
- // Available test tasks
62
- const taskOptions = ref([
63
- { label: 'Build Shelter', value: 'buildShelter' },
64
- { label: 'Get Wood', value: 'getWood' },
65
- { label: 'Cut Wood', value: 'cutWood' },
66
- { label: 'Make Planks', value: 'makePlanks' },
67
- { label: 'Build Wall', value: 'buildWall' },
68
- { label: 'Build Roof', value: 'buildRoof' }
69
- ])
70
-
71
- const schedulesPathFunction = computed(() => (range) =>
72
- path.cron.schedules({ ...reverseRange(range) })
73
- )
74
-
75
- const intervalsPathFunction = computed(() => (range) =>
76
- path.cron.intervals({ ...reverseRange(range) })
77
- )
78
-
79
- const services = computed(() => Object.keys(api.metadata.api.value.services))
80
-
81
-
82
19
 
83
- </script>
20
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div class="w-full px-4 py-5">
3
+
4
+ <AdminTopMenu />
5
+
6
+ <div class="bg-surface-0 dark:bg-surface-900 p-3 shadow mb-1">
7
+ <ActionForm
8
+ service="cron"
9
+ action="setInterval" />
10
+
11
+ </div>
12
+
13
+
14
+ <div class="bg-surface-0 dark:bg-surface-900 p-3 shadow mb-1">
15
+ <h3>{{ t('cron.intervals') }}</h3>
16
+ <range-viewer :pathFunction="intervalsPathFunction" key="intervals"
17
+ :canLoadTop="false" :canDropBottom="false"
18
+ loadBottomSensorSize="3000px" dropBottomSensorSize="12000px">
19
+ <template #empty>
20
+ <div class="bg-surface-0 p-3 shadow text-center text-gray-500 text-lg">
21
+ No intervals found...
22
+ </div>
23
+ </template>
24
+
25
+ <template #default="{ item: interval }">
26
+ <IntervalCard :interval="interval" class="mt-1" />
27
+ </template>
28
+ </range-viewer>
29
+ </div>
30
+
31
+ </div>
32
+ </template>
33
+
34
+ <script setup>
35
+
36
+ import ScheduleCard from '../components/ScheduleCard.vue'
37
+ import IntervalCard from '../components/IntervalCard.vue'
38
+ import Select from 'primevue/select'
39
+ import InputText from 'primevue/inputtext'
40
+ import InputNumber from 'primevue/inputnumber'
41
+ import Dropdown from 'primevue/dropdown'
42
+ import Button from 'primevue/button'
43
+
44
+ import AdminTopMenu from '../components/AdminTopMenu.vue'
45
+ import { ActionForm } from '@live-change/frontend-auto-form'
46
+
47
+ import { useI18n } from 'vue-i18n'
48
+ const { t } = useI18n()
49
+
50
+ import { ref, computed } from 'vue'
51
+ import { RangeViewer } from "@live-change/vue3-components"
52
+
53
+ import { inject } from 'vue'
54
+ const workingZone = inject('workingZone')
55
+
56
+ import { usePath, live, useClient, useActions, reverseRange, useApi } from '@live-change/vue3-ssr'
57
+ const path = usePath()
58
+ const client = useClient()
59
+ const actions = useActions()
60
+ const api = useApi()
61
+
62
+ const intervalsPathFunction = computed(() => (range) =>
63
+ path.cron.intervals({ ...reverseRange(range) })
64
+ .with(interval => path.cron.intervalInfo({ interval: interval.id }).bind('info'))
65
+ .with(interval => path.cron.runState({ jobType: 'cron_Interval', job: interval.id }).bind('runState'))
66
+ .with(interval => path.task.tasksByCauseAndCreatedAt({
67
+ causeType: 'cron_Interval', cause: interval.id, reverse: true, limit: 5
68
+ }).bind('tasks'))
69
+ )
70
+
71
+ const services = computed(() => Object.keys(api.metadata.api.value.services))
72
+
73
+
74
+
75
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div class="w-full px-4 py-5">
3
+
4
+ <AdminTopMenu />
5
+
6
+ <div class="bg-surface-0 dark:bg-surface-900 p-3 shadow mb-1">
7
+ <ActionForm
8
+ service="cron"
9
+ action="setSchedule" />
10
+
11
+ </div>
12
+
13
+
14
+ <div class="bg-surface-0 dark:bg-surface-900 p-3 shadow mb-1">
15
+ <h3>{{ t('cron.schedules') }}</h3>
16
+ <range-viewer :pathFunction="schedulesPathFunction" key="schedules"
17
+ :canLoadTop="false" :canDropBottom="false"
18
+ loadBottomSensorSize="3000px" dropBottomSensorSize="12000px">
19
+ <template #empty>
20
+ <div class="bg-surface-0 p-3 shadow text-center text-gray-500 text-lg">
21
+ No schedules found...
22
+ </div>
23
+ </template>
24
+
25
+ <template #default="{ item: schedule }">
26
+ <ScheduleCard :schedule="schedule" class="mt-1" />
27
+ </template>
28
+ </range-viewer>
29
+ </div>
30
+
31
+ </div>
32
+ </template>
33
+
34
+ <script setup>
35
+
36
+ import ScheduleCard from '../components/ScheduleCard.vue'
37
+ import IntervalCard from '../components/IntervalCard.vue'
38
+ import Select from 'primevue/select'
39
+ import InputText from 'primevue/inputtext'
40
+ import InputNumber from 'primevue/inputnumber'
41
+ import Dropdown from 'primevue/dropdown'
42
+ import Button from 'primevue/button'
43
+
44
+ import AdminTopMenu from '../components/AdminTopMenu.vue'
45
+ import { ActionForm } from '@live-change/frontend-auto-form'
46
+
47
+ import { useI18n } from 'vue-i18n'
48
+ const { t } = useI18n()
49
+
50
+ import { ref, computed } from 'vue'
51
+ import { RangeViewer } from "@live-change/vue3-components"
52
+
53
+ import { inject } from 'vue'
54
+ const workingZone = inject('workingZone')
55
+
56
+ import { usePath, live, useClient, useActions, reverseRange, useApi } from '@live-change/vue3-ssr'
57
+ const path = usePath()
58
+ const client = useClient()
59
+ const actions = useActions()
60
+ const api = useApi()
61
+
62
+ const schedulesPathFunction = computed(() => (range) =>
63
+ path.cron.schedules({ ...reverseRange(range) })
64
+ .with(schedule => path.cron.scheduleInfo({ schedule: schedule.id }).bind('info'))
65
+ .with(schedule => path.cron.runState({ jobType: 'cron_Schedule', job: schedule.id }).bind('runState'))
66
+ .with(schedule => path.task.tasksByCauseAndCreatedAt({
67
+ causeType: 'cron_Schedule', cause: schedule.id, reverse: true, limit: 5
68
+ }).bind('tasks'))
69
+ )
70
+
71
+ const services = computed(() => Object.keys(api.metadata.api.value.services))
72
+
73
+
74
+
75
+ </script>
@@ -3,7 +3,7 @@ export function taskAdminRoutes(config = {}) {
3
3
  return [
4
4
 
5
5
  route({
6
- name: 'task:admin', path: prefix, meta: { },
6
+ name: 'task:admin', path: prefix, meta: { requireRoles: ['admin'] },
7
7
  component: () => import("./pages/TaskAdmin.vue"),
8
8
  props: true
9
9
  }),
@@ -14,10 +14,20 @@ export function cronAdminRoutes(config = {}) {
14
14
  const { prefix = '/', route = (r) => r } = config
15
15
  return [
16
16
  route({
17
- name: 'cron:admin', path: prefix, meta: { },
17
+ name: 'cron:admin', path: prefix, meta: { requireRoles: ['admin'] },
18
18
  component: () => import("./pages/CronAdmin.vue"),
19
19
  props: true
20
20
  }),
21
+ route({
22
+ name: 'cron:admin:intervals', path: prefix+'/intervals', meta: { requireRoles: ['admin'] },
23
+ component: () => import("./pages/CronIntervalsAdmin.vue"),
24
+ props: true
25
+ }),
26
+ route({
27
+ name: 'cron:admin:schedules', path: prefix+'/schedules', meta: { requireRoles: ['admin'] },
28
+ component: () => import("./pages/CronSchedulesAdmin.vue"),
29
+ props: true
30
+ }),
21
31
  ]
22
32
  }
23
33
 
@@ -56,8 +66,8 @@ export function routes(config = {}) {
56
66
 
57
67
  ...contentEditRoutes({ ...config }),
58
68
 
59
- ...dbAdminRoutes({ prefix: '/_db', route: r => ({ ...r, meta: { ...r.meta, raw: true }}) }),
60
- ...taskAdminRoutes({ prefix: '/_task', route: r => ({ ...r, meta: { ...r.meta, raw: true }}) }),
69
+ ...dbAdminRoutes({ prefix: '/_db', route: r => ({ ...r, meta: { ...r.meta, pageType: 'wide' }}) }),
70
+ ...taskAdminRoutes({ prefix: '/_task', route: r => ({ ...r, meta: { ...r.meta, pageType: 'wide' }}) }),
61
71
  ...cronAdminRoutes({ prefix: '/_cron', route: r => ({ ...r, meta: { ...r.meta, pageType: 'wide' }}) }),
62
72
  ...catchAllPagesRoute({ ...config }),
63
73
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/task-frontend",
3
- "version": "0.9.162",
3
+ "version": "0.9.164",
4
4
  "scripts": {
5
5
  "memDev": "tsx --inspect --expose-gc server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
6
6
  "localDevInit": "tsx server/start.js localDev --enableSessions --initScript ./init.js --dbAccess",
@@ -37,43 +37,43 @@
37
37
  "@codemirror/language": "6.10.1",
38
38
  "@dotenvx/dotenvx": "0.27.0",
39
39
  "@fortawesome/fontawesome-free": "^6.7.2",
40
- "@live-change/access-control-frontend": "^0.9.162",
41
- "@live-change/access-control-service": "^0.9.162",
42
- "@live-change/backup-service": "^0.9.162",
43
- "@live-change/blog-frontend": "^0.9.162",
44
- "@live-change/blog-service": "^0.9.162",
45
- "@live-change/cli": "^0.9.162",
46
- "@live-change/content-frontend": "^0.9.162",
47
- "@live-change/content-service": "^0.9.162",
48
- "@live-change/cron-service": "^0.9.162",
49
- "@live-change/dao": "^0.9.162",
50
- "@live-change/dao-vue3": "^0.9.162",
51
- "@live-change/dao-websocket": "^0.9.162",
52
- "@live-change/db-client": "^0.9.162",
53
- "@live-change/draft-service": "^0.9.162",
54
- "@live-change/email-service": "^0.9.162",
55
- "@live-change/framework": "^0.9.162",
56
- "@live-change/frontend-auto-form": "^0.9.162",
57
- "@live-change/frontend-base": "^0.9.162",
58
- "@live-change/geoip-service": "^0.9.162",
59
- "@live-change/image-frontend": "^0.9.162",
60
- "@live-change/locale-settings-service": "^0.9.162",
61
- "@live-change/password-authentication-service": "^0.9.162",
62
- "@live-change/prosemirror-service": "^0.9.162",
63
- "@live-change/secret-code-service": "^0.9.162",
64
- "@live-change/secret-link-service": "^0.9.162",
65
- "@live-change/session-service": "^0.9.162",
66
- "@live-change/task-service": "^0.9.162",
67
- "@live-change/upload-frontend": "^0.9.162",
68
- "@live-change/url-frontend": "^0.9.162",
69
- "@live-change/url-service": "^0.9.162",
70
- "@live-change/user-frontend": "^0.9.162",
71
- "@live-change/user-identification-service": "^0.9.162",
72
- "@live-change/user-service": "^0.9.162",
73
- "@live-change/vote-service": "^0.9.162",
74
- "@live-change/vue3-components": "^0.9.162",
75
- "@live-change/vue3-ssr": "^0.9.162",
76
- "@live-change/wysiwyg-frontend": "^0.9.162",
40
+ "@live-change/access-control-frontend": "^0.9.164",
41
+ "@live-change/access-control-service": "^0.9.164",
42
+ "@live-change/backup-service": "^0.9.164",
43
+ "@live-change/blog-frontend": "^0.9.164",
44
+ "@live-change/blog-service": "^0.9.164",
45
+ "@live-change/cli": "^0.9.164",
46
+ "@live-change/content-frontend": "^0.9.164",
47
+ "@live-change/content-service": "^0.9.164",
48
+ "@live-change/cron-service": "^0.9.164",
49
+ "@live-change/dao": "^0.9.164",
50
+ "@live-change/dao-vue3": "^0.9.164",
51
+ "@live-change/dao-websocket": "^0.9.164",
52
+ "@live-change/db-client": "^0.9.164",
53
+ "@live-change/draft-service": "^0.9.164",
54
+ "@live-change/email-service": "^0.9.164",
55
+ "@live-change/framework": "^0.9.164",
56
+ "@live-change/frontend-auto-form": "^0.9.164",
57
+ "@live-change/frontend-base": "^0.9.164",
58
+ "@live-change/geoip-service": "^0.9.164",
59
+ "@live-change/image-frontend": "^0.9.164",
60
+ "@live-change/locale-settings-service": "^0.9.164",
61
+ "@live-change/password-authentication-service": "^0.9.164",
62
+ "@live-change/prosemirror-service": "^0.9.164",
63
+ "@live-change/secret-code-service": "^0.9.164",
64
+ "@live-change/secret-link-service": "^0.9.164",
65
+ "@live-change/session-service": "^0.9.164",
66
+ "@live-change/task-service": "^0.9.164",
67
+ "@live-change/upload-frontend": "^0.9.164",
68
+ "@live-change/url-frontend": "^0.9.164",
69
+ "@live-change/url-service": "^0.9.164",
70
+ "@live-change/user-frontend": "^0.9.164",
71
+ "@live-change/user-identification-service": "^0.9.164",
72
+ "@live-change/user-service": "^0.9.164",
73
+ "@live-change/vote-service": "^0.9.164",
74
+ "@live-change/vue3-components": "^0.9.164",
75
+ "@live-change/vue3-ssr": "^0.9.164",
76
+ "@live-change/wysiwyg-frontend": "^0.9.164",
77
77
  "@vueuse/core": "^12.3.0",
78
78
  "codeceptjs-assert": "^0.0.5",
79
79
  "compression": "^1.7.5",
@@ -95,7 +95,7 @@
95
95
  "vue3-scroll-border": "0.1.7"
96
96
  },
97
97
  "devDependencies": {
98
- "@live-change/codeceptjs-helper": "^0.9.162",
98
+ "@live-change/codeceptjs-helper": "^0.9.164",
99
99
  "codeceptjs": "^3.6.10",
100
100
  "generate-password": "1.7.1",
101
101
  "playwright": "1.49.1",
@@ -106,5 +106,5 @@
106
106
  "author": "Michał Łaszczewski <michal@laszczewski.pl>",
107
107
  "license": "ISC",
108
108
  "description": "",
109
- "gitHead": "59cd1485ca6d76bd87a6634a92d6e661e5803d5e"
109
+ "gitHead": "38f9fb8b01a9527d8f6036e174edd1fa41443301"
110
110
  }
@@ -8,6 +8,9 @@ import documentTypePage from './page.documentType.js'
8
8
 
9
9
  app.config = {
10
10
  services: [
11
+ {
12
+ name: 'timer',
13
+ },
11
14
  {
12
15
  name: 'session',
13
16
  createSessionOnUpdate: true
@@ -1,3 +1,4 @@
1
+ import timer from '@live-change/timer-service'
1
2
  import session from '@live-change/session-service'
2
3
  import user from '@live-change/user-service'
3
4
  import email from '@live-change/email-service'
@@ -25,6 +26,7 @@ import cron from '@live-change/cron-service'
25
26
  import init from './init.js'
26
27
 
27
28
  export {
29
+ timer,
28
30
  session,
29
31
  user,
30
32
  email,
@@ -22,12 +22,45 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
22
22
 
23
23
  const workerQueue = new PQueue({ concurrency: workersCount })
24
24
 
25
+ const woodTypes = ['oak', 'birch', 'spruce', 'acacia']
26
+
27
+
28
+ const wait = task({
29
+ name: 'wait',
30
+ properties: {
31
+ duration: {
32
+ type: Number,
33
+ validation: ['nonEmpty', 'integer'],
34
+ input: 'integer',
35
+ inputConfig: {
36
+ attributes: {
37
+ suffix: ' ms',
38
+ showButtons: true,
39
+ step: 1000,
40
+ min: 0,
41
+ }
42
+ },
43
+ defaultValue: 10000
44
+ }
45
+ },
46
+ returns: {
47
+ type: 'void',
48
+ },
49
+ async execute({ duration }, { service, task }, emit) {
50
+ console.log("WAIT", duration)
51
+ await sleep(duration)
52
+ }
53
+ }, definition)
54
+
55
+
25
56
  const getWood = task({
26
57
  name: 'getWood',
27
58
  properties: {
28
59
  woodType: {
29
60
  type: String,
30
- validation: ['nonEmpty']
61
+ validation: ['nonEmpty'],
62
+ options: woodTypes,
63
+ input: 'select'
31
64
  },
32
65
  },
33
66
  returns: {
@@ -39,6 +72,7 @@ const getWood = task({
39
72
  }
40
73
  },
41
74
  async execute({ woodType }, { service, task }, emit) {
75
+ console.log("GET WOOD", woodType)
42
76
  task.progress(0, 1, 'finding tree')
43
77
  await sleep(workDuration)
44
78
  if(Math.random() < 0.1) {
@@ -61,7 +95,9 @@ const cutWood = task({
61
95
  type: 'Wood',
62
96
  properties: {
63
97
  woodType: {
64
- type: String
98
+ type: String,
99
+ options: woodTypes,
100
+ input: 'select'
65
101
  }
66
102
  }
67
103
  },