@whyour/qinglong 2.20.2-3 → 2.21.0-10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/.env.example +5 -0
  2. package/AGENTS.md +43 -0
  3. package/README-en.md +11 -0
  4. package/README.md +11 -0
  5. package/back/protos/api.proto +3 -0
  6. package/deploy/kubernetes/README.md +81 -0
  7. package/deploy/kubernetes/base/kustomization.yaml +5 -0
  8. package/deploy/kubernetes/base/qinglong.yaml +95 -0
  9. package/deploy/kubernetes/overlays/example/kustomization.yaml +10 -0
  10. package/docker/Dockerfile +51 -69
  11. package/docker/Dockerfile.310 +94 -0
  12. package/docker/Dockerfile.debian +120 -0
  13. package/docker/{310.Dockerfile → Dockerfile.debian310} +13 -5
  14. package/docker/docker-entrypoint.sh +95 -3
  15. package/docs/PROJECT_ARCHITECTURE.md +550 -0
  16. package/package.json +29 -28
  17. package/sample/config.sample.sh +19 -3
  18. package/sample/notify.js +145 -9
  19. package/sample/notify.py +110 -11
  20. package/shell/api.sh +51 -21
  21. package/shell/bot.sh +30 -16
  22. package/shell/check.sh +21 -19
  23. package/shell/lang/en.sh +105 -0
  24. package/shell/lang/zh.sh +105 -0
  25. package/shell/otask.sh +117 -28
  26. package/shell/preload/client.js +20 -2
  27. package/shell/preload/esm-loader.mjs +41 -0
  28. package/shell/preload/sitecustomize.js +36 -0
  29. package/shell/pub.sh +8 -8
  30. package/shell/rmlog.sh +5 -5
  31. package/shell/share.sh +50 -31
  32. package/shell/start.sh +40 -27
  33. package/shell/task.sh +10 -2
  34. package/shell/update.sh +22 -22
  35. package/static/build/api/config.js +17 -7
  36. package/static/build/api/cron.js +40 -3
  37. package/static/build/api/dashboard.js +344 -0
  38. package/static/build/api/env.js +40 -2
  39. package/static/build/api/index.js +2 -0
  40. package/static/build/api/log.js +15 -5
  41. package/static/build/api/script.js +34 -9
  42. package/static/build/api/system.js +22 -8
  43. package/static/build/api/user.js +6 -5
  44. package/static/build/app.js +22 -19
  45. package/static/build/config/const.js +8 -6
  46. package/static/build/config/container.js +12 -0
  47. package/static/build/config/grpcCerts.js +150 -0
  48. package/static/build/config/index.js +6 -4
  49. package/static/build/config/util.js +69 -18
  50. package/static/build/data/cron.js +3 -1
  51. package/static/build/data/cronStats.js +59 -0
  52. package/static/build/data/env.js +2 -0
  53. package/static/build/data/notify.js +18 -1
  54. package/static/build/data/runningInstance.js +57 -0
  55. package/static/build/data/sock.js +1 -0
  56. package/static/build/loaders/app.js +7 -7
  57. package/static/build/loaders/db.js +8 -2
  58. package/static/build/loaders/deps.js +1 -22
  59. package/static/build/loaders/express.js +19 -15
  60. package/static/build/loaders/initData.js +20 -2
  61. package/static/build/loaders/initFile.js +3 -1
  62. package/static/build/loaders/initTask.js +3 -2
  63. package/static/build/loaders/server.js +1 -1
  64. package/static/build/protos/api.js +23 -1
  65. package/static/build/schedule/addCron.js +67 -10
  66. package/static/build/schedule/api.js +1 -1
  67. package/static/build/schedule/client.js +9 -1
  68. package/static/build/schedule/delCron.js +11 -1
  69. package/static/build/schedule/health.js +1 -1
  70. package/static/build/services/config.js +19 -5
  71. package/static/build/services/cron.js +198 -38
  72. package/static/build/services/dependence.js +19 -18
  73. package/static/build/services/env.js +31 -3
  74. package/static/build/services/grpc.js +32 -6
  75. package/static/build/services/http.js +33 -18
  76. package/static/build/services/notify.js +87 -8
  77. package/static/build/services/open.js +2 -1
  78. package/static/build/services/sshKey.js +6 -3
  79. package/static/build/services/subscription.js +8 -5
  80. package/static/build/services/system.js +34 -9
  81. package/static/build/services/user.js +39 -13
  82. package/static/build/shared/auth.js +7 -1
  83. package/static/build/shared/i18n.js +160 -0
  84. package/static/build/shared/pLimit.js +15 -3
  85. package/static/build/shared/runCron.js +5 -0
  86. package/static/build/shared/store.js +7 -0
  87. package/static/build/validation/schedule.js +10 -0
  88. package/static/dist/1089.0ecc5383.async.js +1 -0
  89. package/static/dist/1147.62793604.async.js +1 -0
  90. package/static/dist/1192.4e5740f2.async.js +1 -0
  91. package/static/dist/1214.c6469b53.async.js +1 -0
  92. package/static/dist/134.54df9e38.async.js +1 -0
  93. package/static/dist/1352.96a77e1d.async.js +1 -0
  94. package/static/dist/{1765.d8e002d7.async.js → 1765.5ac01a93.async.js} +1 -1
  95. package/static/dist/1836.f0b74cf1.async.js +1 -0
  96. package/static/dist/{1885.e0d00d2d.async.js → 1885.e1ea09f6.async.js} +1 -1
  97. package/static/dist/1967.3f3945d0.async.js +1 -0
  98. package/static/dist/{2096.383c1047.async.js → 2096.6d98fd12.async.js} +1 -1
  99. package/static/dist/2208.85285476.async.js +1 -0
  100. package/static/dist/2286.f371c743.async.js +1 -0
  101. package/static/dist/2476.4e9b0992.async.js +1 -0
  102. package/static/dist/2537.2b262ee0.async.js +1 -0
  103. package/static/dist/{255.12f03ab2.async.js → 255.8a80b983.async.js} +1 -1
  104. package/static/dist/2635.d4c59d23.async.js +1 -0
  105. package/static/dist/2808.cdc0995c.async.js +1 -0
  106. package/static/dist/{2821.be3dc88e.async.js → 2821.651f31e5.async.js} +1 -1
  107. package/static/dist/2986.4c49eef7.async.js +1 -0
  108. package/static/dist/300.08ac9875.async.js +1 -0
  109. package/static/dist/3191.545ab873.async.js +1 -0
  110. package/static/dist/3714.8d6dba9e.async.js +1 -0
  111. package/static/dist/3906.a5eee612.async.js +1 -0
  112. package/static/dist/{6541.a6d499de.async.js → 3948.4fe809fd.async.js} +1 -1
  113. package/static/dist/{5171.7fc6d0a2.async.js → 4573.16f19278.async.js} +1 -1
  114. package/static/dist/{4859.93e63ea6.async.js → 4859.6592cebb.async.js} +1 -1
  115. package/static/dist/4887.8e3ae573.async.js +1 -0
  116. package/static/dist/{4902.54ecbdb5.async.js → 4902.555f92ab.async.js} +1 -1
  117. package/static/dist/{4934.1ca6b6b0.async.js → 4934.f3539d08.async.js} +1 -1
  118. package/static/dist/5077.cbd111cd.async.js +1 -0
  119. package/static/dist/{6247.b550d996.async.js → 5157.7c5af144.async.js} +1 -1
  120. package/static/dist/{540.481d4708.async.js → 540.19ddf020.async.js} +1 -1
  121. package/static/dist/{5970.10ac4f16.async.js → 5970.d3a6e7bd.async.js} +1 -1
  122. package/static/dist/{6013.2d7bb12a.async.js → 6013.4e38de36.async.js} +1 -1
  123. package/static/dist/{6016.9c379049.async.js → 6016.1e6574b6.async.js} +1 -1
  124. package/static/dist/{6035.5889ddc7.async.js → 6035.32ebfad9.async.js} +1 -1
  125. package/static/dist/6339.ab305e96.async.js +1 -0
  126. package/static/dist/6569.55177f6a.async.js +1 -0
  127. package/static/dist/6610.c111af7e.async.js +1 -0
  128. package/static/dist/{3034.6413c38e.async.js → 6638.87a163d2.async.js} +1 -1
  129. package/static/dist/{6646.5fc37228.async.js → 6646.95e92533.async.js} +1 -1
  130. package/static/dist/6665.1d099ad1.async.js +1 -0
  131. package/static/dist/{7355.b7c0562f.async.js → 7355.f9f263d8.async.js} +1 -1
  132. package/static/dist/{7384.065ccae2.async.js → 7384.1bb449d5.async.js} +1 -1
  133. package/static/dist/7441.ee1bf2bb.async.js +1 -0
  134. package/static/dist/{7508.a31662a3.async.js → 7508.7dcee91c.async.js} +1 -1
  135. package/static/dist/{7802.6b73f16a.async.js → 7802.34255805.async.js} +1 -1
  136. package/static/dist/{7984.e6bb9378.async.js → 7984.594296a5.async.js} +1 -1
  137. package/static/dist/{5653.4fce7ce8.async.js → 8033.5cb31493.async.js} +1 -1
  138. package/static/dist/8147.2fb55202.async.js +1 -0
  139. package/static/dist/8187.48c130d6.async.js +1 -0
  140. package/static/dist/8495.d53e15ca.async.js +1 -0
  141. package/static/dist/8587.315b06c0.async.js +1 -0
  142. package/static/dist/8826.db34ad8d.async.js +1 -0
  143. package/static/dist/{2742.4852aac8.async.js → 8865.ed665d31.async.js} +1 -1
  144. package/static/dist/901.7ee5c6d3.async.js +1 -0
  145. package/static/dist/9271.231db2ce.async.js +1 -0
  146. package/static/dist/9323.a33f47da.async.js +1 -0
  147. package/static/dist/{9730.30083c91.async.js → 9730.801665a3.async.js} +1 -1
  148. package/static/dist/{9761.627ca3b5.async.js → 9761.360a19d8.async.js} +1 -1
  149. package/static/dist/index.html +2 -2
  150. package/static/dist/layouts__index.a7a2bfe0.async.js +1 -0
  151. package/static/dist/layouts__index.adf0692f.chunk.css +1 -0
  152. package/static/dist/preload_helper.3048fe5b.js +1 -0
  153. package/static/dist/{src__pages__config__index.622b6ee8.async.js → src__pages__config__index.a22ff7dc.async.js} +1 -1
  154. package/static/dist/{src__pages__crontab__const.323d5124.async.js → src__pages__crontab__const.aba07deb.async.js} +1 -1
  155. package/static/dist/src__pages__crontab__detail.b98f0084.async.js +1 -0
  156. package/static/dist/src__pages__crontab__index.68e78a33.async.js +1 -0
  157. package/static/dist/{src__pages__crontab__logModal.5e6a4bf2.async.js → src__pages__crontab__logModal.831d5e4b.async.js} +1 -1
  158. package/static/dist/src__pages__crontab__modal.f7041c7c.async.js +1 -0
  159. package/static/dist/src__pages__crontab__type.d7af36e5.async.js +1 -0
  160. package/static/dist/{src__pages__crontab__viewCreateModal.ffcf7a24.async.js → src__pages__crontab__viewCreateModal.4d589f66.async.js} +1 -1
  161. package/static/dist/{src__pages__crontab__viewManageModal.c2724575.async.js → src__pages__crontab__viewManageModal.0e317746.async.js} +1 -1
  162. package/static/dist/src__pages__dashboard__index.58177357.async.js +1 -0
  163. package/static/dist/src__pages__dependence__index.b7bbc795.async.js +1 -0
  164. package/static/dist/{src__pages__dependence__logModal.f123e2ac.async.js → src__pages__dependence__logModal.0681830b.async.js} +1 -1
  165. package/static/dist/src__pages__dependence__modal.f7bb47ba.async.js +1 -0
  166. package/static/dist/src__pages__env__editNameModal.48715576.async.js +1 -0
  167. package/static/dist/src__pages__env__index.452cdc47.async.js +1 -0
  168. package/static/dist/src__pages__env__modal.e1234834.async.js +1 -0
  169. package/static/dist/src__pages__error__index.dedb2b90.async.js +1 -0
  170. package/static/dist/src__pages__initialization__index.22620a77.async.js +1 -0
  171. package/static/dist/src__pages__log__index.9c66639f.async.js +1 -0
  172. package/static/dist/{src__pages__login__index.cd6e3152.async.js → src__pages__login__index.8c813eb1.async.js} +1 -1
  173. package/static/dist/{src__pages__script__components__UnsupportedFilePreview__index.39074c68.async.js → src__pages__script__components__UnsupportedFilePreview__index.c347a13a.async.js} +1 -1
  174. package/static/dist/src__pages__script__editNameModal.532a3044.async.js +1 -0
  175. package/static/dist/src__pages__script__index.1fc20f22.async.js +1 -0
  176. package/static/dist/src__pages__script__renameModal.5cdde919.async.js +1 -0
  177. package/static/dist/src__pages__script__saveModal.5ffed35f.async.js +1 -0
  178. package/static/dist/src__pages__script__setting.1a82c6d7.async.js +1 -0
  179. package/static/dist/src__pages__setting__appModal.43b7758a.async.js +1 -0
  180. package/static/dist/src__pages__setting__checkUpdate.34d773f2.async.js +1 -0
  181. package/static/dist/src__pages__setting__dependence.9c04c79a.async.js +1 -0
  182. package/static/dist/src__pages__setting__index.7e070838.async.js +1 -0
  183. package/static/dist/src__pages__setting__notification.bac481c2.async.js +1 -0
  184. package/static/dist/src__pages__setting__other.707cfafe.async.js +1 -0
  185. package/static/dist/src__pages__setting__security.b7235753.async.js +1 -0
  186. package/static/dist/src__pages__setting__systemLog.b81a8c1d.async.js +1 -0
  187. package/static/dist/{src__pages__subscription__logModal.0caa7283.async.js → src__pages__subscription__logModal.3864b37f.async.js} +1 -1
  188. package/static/dist/src__pages__subscription__modal.562448e2.async.js +1 -0
  189. package/static/dist/{umi.ef8199a4.js → umi.a54a59f6.js} +1 -1
  190. package/version.yaml +33 -4
  191. package/sample/notify.py.save +0 -1010
  192. package/static/dist/105.85a5c47a.async.js +0 -1
  193. package/static/dist/1083.f86ce804.async.js +0 -1
  194. package/static/dist/1147.32f41a88.async.js +0 -1
  195. package/static/dist/1352.ab6da08e.async.js +0 -1
  196. package/static/dist/1690.f0290540.async.js +0 -1
  197. package/static/dist/1742.6cbe5aca.async.js +0 -1
  198. package/static/dist/2208.8e3a7325.async.js +0 -1
  199. package/static/dist/3191.70bc19db.async.js +0 -1
  200. package/static/dist/5312.74b95311.async.js +0 -1
  201. package/static/dist/5691.931f59c5.async.js +0 -1
  202. package/static/dist/6159.55cb068a.async.js +0 -1
  203. package/static/dist/7025.f4080d63.async.js +0 -1
  204. package/static/dist/739.6be5552a.async.js +0 -1
  205. package/static/dist/7571.4f6240b1.async.js +0 -1
  206. package/static/dist/786.59fc381c.async.js +0 -1
  207. package/static/dist/8317.c44c1ebd.async.js +0 -1
  208. package/static/dist/8826.0291edfd.async.js +0 -1
  209. package/static/dist/955.3c9481f7.async.js +0 -1
  210. package/static/dist/layouts__index.1fce90e0.chunk.css +0 -1
  211. package/static/dist/layouts__index.8dcf1576.async.js +0 -1
  212. package/static/dist/preload_helper.116a62f6.js +0 -1
  213. package/static/dist/src__pages__crontab__detail.b07f0c0a.async.js +0 -1
  214. package/static/dist/src__pages__crontab__index.2e2e1096.async.js +0 -1
  215. package/static/dist/src__pages__crontab__modal.4d8c2a22.async.js +0 -1
  216. package/static/dist/src__pages__crontab__type.db7c1858.async.js +0 -1
  217. package/static/dist/src__pages__dependence__index.3d87ceac.async.js +0 -1
  218. package/static/dist/src__pages__dependence__modal.631ffb5b.async.js +0 -1
  219. package/static/dist/src__pages__env__editNameModal.ff85ef8c.async.js +0 -1
  220. package/static/dist/src__pages__env__index.a0a2fece.async.js +0 -1
  221. package/static/dist/src__pages__env__modal.d1004662.async.js +0 -1
  222. package/static/dist/src__pages__error__index.1bc3c90b.async.js +0 -1
  223. package/static/dist/src__pages__initialization__index.8b1cbaf9.async.js +0 -1
  224. package/static/dist/src__pages__log__index.cf00c9af.async.js +0 -1
  225. package/static/dist/src__pages__script__editNameModal.53424d49.async.js +0 -1
  226. package/static/dist/src__pages__script__index.e65df827.async.js +0 -1
  227. package/static/dist/src__pages__script__renameModal.4bbe7fb1.async.js +0 -1
  228. package/static/dist/src__pages__script__saveModal.cf449f3c.async.js +0 -1
  229. package/static/dist/src__pages__script__setting.b345d59a.async.js +0 -1
  230. package/static/dist/src__pages__setting__appModal.03faec89.async.js +0 -1
  231. package/static/dist/src__pages__setting__checkUpdate.46efe057.async.js +0 -1
  232. package/static/dist/src__pages__setting__dependence.4495c7b6.async.js +0 -1
  233. package/static/dist/src__pages__setting__index.6919c399.async.js +0 -1
  234. package/static/dist/src__pages__setting__notification.d6a3884f.async.js +0 -1
  235. package/static/dist/src__pages__setting__other.0d931d6f.async.js +0 -1
  236. package/static/dist/src__pages__setting__security.91cb545f.async.js +0 -1
  237. package/static/dist/src__pages__setting__systemLog.1d433a48.async.js +0 -1
  238. package/static/dist/src__pages__subscription__modal.0b84f6cf.async.js +0 -1
package/shell/task.sh CHANGED
@@ -4,9 +4,17 @@ dir_shell=$QL_DIR/shell
4
4
  . $dir_shell/share.sh
5
5
  . $dir_shell/api.sh
6
6
 
7
- trap "single_hanle" 2 3 20 15 14 19 1
7
+ trap 'single_hanle SIGINT' INT
8
+ trap 'single_hanle SIGTERM' TERM
9
+ trap 'single_hanle SIGHUP' HUP
10
+ trap 'single_hanle SIGALRM' ALRM
11
+ trap 'single_hanle SIGTSTP' TSTP
12
+ trap 'single_hanle SIGQUIT' QUIT
13
+
8
14
  single_hanle() {
9
- eval MANUAL=true handle_task_end "$@" "$cmd"
15
+ _task_exit_code="${_task_exit_code:-$?}"
16
+ [[ "$_task_exit_code" == "0" ]] && _task_exit_code="1"
17
+ eval MANUAL=true handle_task_end "$@"
10
18
  exit 1
11
19
  }
12
20
 
package/shell/update.sh CHANGED
@@ -33,7 +33,7 @@ output_list_add_drop() {
33
33
  local list=$1
34
34
  local type=$2
35
35
  if [[ -s $list ]]; then
36
- echo -e "检测到有${type}的定时任务:"
36
+ t '检测到有%s的定时任务:' "$type"
37
37
  cat $list
38
38
  fi
39
39
  }
@@ -45,7 +45,7 @@ del_cron() {
45
45
  local path=$2
46
46
  local detail=""
47
47
  local ids=""
48
- echo -e "\n开始尝试自动删除失效的定时任务..."
48
+ t '\n开始尝试自动删除失效的定时任务...'
49
49
  for cron in $(cat $list_drop); do
50
50
  local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
51
51
  if [[ $ids ]]; then
@@ -76,7 +76,7 @@ del_cron() {
76
76
  add_cron() {
77
77
  local list_add=$1
78
78
  local path=$2
79
- echo -e "\n开始尝试自动添加定时任务..."
79
+ t '\n开始尝试自动添加定时任务...'
80
80
  local detail=""
81
81
  cd $dir_scripts
82
82
  for file in $(cat $list_add); do
@@ -136,10 +136,10 @@ update_repo() {
136
136
  git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
137
137
 
138
138
  if [[ $exit_status -eq 0 ]]; then
139
- echo -e "拉取 ${uniq_path} 成功...\n"
139
+ t '拉取 %s 成功...\n' "${uniq_path}"
140
140
  diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
141
141
  else
142
- echo -e "拉取 ${uniq_path} 失败,请检查日志...\n"
142
+ t '拉取 %s 失败,请检查日志...\n' "${uniq_path}"
143
143
  fi
144
144
  }
145
145
 
@@ -160,7 +160,7 @@ update_raw() {
160
160
  local raw_url="$url"
161
161
  local suffix="${raw_url##*.}"
162
162
  local raw_file_name="${uniq_path}.${suffix}"
163
- echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
163
+ t '开始下载:%s 保存路径:%s\n' "${raw_url}" "$dir_raw/${raw_file_name}"
164
164
 
165
165
  set_proxy "$proxy"
166
166
  wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
@@ -169,7 +169,7 @@ update_raw() {
169
169
 
170
170
  if [[ $? -eq 0 ]]; then
171
171
  mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
172
- echo -e "下载 ${raw_file_name} 成功...\n"
172
+ t '下载 %s 成功...\n' "${raw_file_name}"
173
173
  cd $dir_raw
174
174
  local filename="raw_${raw_file_name}"
175
175
  local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
@@ -196,7 +196,7 @@ update_raw() {
196
196
  # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
197
197
  fi
198
198
  else
199
- echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
199
+ t '下载 %s 失败,保留之前正常下载的版本...\n' "${raw_file_name}"
200
200
  [[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
201
201
  fi
202
202
 
@@ -207,13 +207,13 @@ run_extra_shell() {
207
207
  if [[ -f $file_extra_shell ]]; then
208
208
  . $file_extra_shell
209
209
  else
210
- echo -e "$file_extra_shell文件不存在,跳过执行...\n"
210
+ t '%s文件不存在,跳过执行...\n' "$file_extra_shell"
211
211
  fi
212
212
  }
213
213
 
214
214
  ## 脚本用法
215
215
  usage() {
216
- echo -e "$cmd_update 命令使用方法:"
216
+ t "$cmd_update 命令使用方法:"
217
217
  echo -e "1. $cmd_update update # 更新并重启青龙"
218
218
  echo -e "2. $cmd_update extra # 运行自定义脚本"
219
219
  echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
@@ -268,7 +268,7 @@ update_qinglong() {
268
268
  downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
269
269
  downloadStaticUrl="https://github.com/whyour/qinglong-static/archive/refs/heads"
270
270
  fi
271
- echo -e "使用 ${mirror} 源更新...\n"
271
+ t '使用 %s 源更新...\n' "${mirror}"
272
272
 
273
273
  local primary_branch="master"
274
274
  if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then
@@ -279,13 +279,13 @@ update_qinglong() {
279
279
  exit_status=$?
280
280
 
281
281
  if [[ $exit_status -eq 0 ]]; then
282
- echo -e "更新青龙源文件成功...\n"
282
+ t '更新青龙源文件成功...\n'
283
283
 
284
284
  unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
285
285
 
286
286
  update_qinglong_static
287
287
  else
288
- echo -e "更新青龙源文件失败,请检查网络...\n"
288
+ t '更新青龙源文件失败,请检查网络...\n'
289
289
  fi
290
290
  }
291
291
 
@@ -294,30 +294,30 @@ update_qinglong_static() {
294
294
  exit_status=$?
295
295
 
296
296
  if [[ $exit_status -eq 0 ]]; then
297
- echo -e "更新青龙静态资源成功...\n"
297
+ t '更新青龙静态资源成功...\n'
298
298
  unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
299
299
 
300
300
  check_update_dep
301
301
  else
302
- echo -e "更新青龙静态资源失败,请检查网络...\n"
302
+ t '更新青龙静态资源失败,请检查网络...\n'
303
303
  fi
304
304
  }
305
305
 
306
306
  check_update_dep() {
307
- echo -e "\n开始检测依赖...\n"
307
+ t '\n开始检测依赖...\n'
308
308
  if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
309
309
  npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
310
310
  fi
311
311
 
312
312
  if [[ $exit_status -eq 0 ]]; then
313
- echo -e "\n依赖检测安装成功...\n"
314
- echo -e "更新包下载成功..."
313
+ t '\n依赖检测安装成功...\n'
314
+ t '更新包下载成功...\n'
315
315
 
316
316
  if [[ "$needRestart" == 'true' ]]; then
317
317
  reload_qinglong "system"
318
318
  fi
319
319
  else
320
- echo -e "\n依赖检测安装失败,请检查网络...\n"
320
+ t '\n依赖检测安装失败,请检查网络...\n'
321
321
  fi
322
322
  }
323
323
 
@@ -514,7 +514,7 @@ main() {
514
514
  if [[ -n $p2 ]]; then
515
515
  update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" "$p9" "$p10"
516
516
  else
517
- eval echo -e "命令输入错误...\\\n" $cmd
517
+ t '命令输入错误...\n'
518
518
  eval usage $cmd
519
519
  fi
520
520
  ;;
@@ -523,7 +523,7 @@ main() {
523
523
  if [[ -n $p2 ]]; then
524
524
  update_raw "$p2" "$p3" "$p4" "$p5"
525
525
  else
526
- eval echo -e "命令输入错误...\\\n" $cmd
526
+ t '命令输入错误...\n'
527
527
  eval usage $cmd
528
528
  fi
529
529
  ;;
@@ -549,7 +549,7 @@ main() {
549
549
  eval update_auth_config "\\\"username\\\":\\\"$p2\\\"" "重置用户名" $cmd
550
550
  ;;
551
551
  *)
552
- eval echo -e "命令输入错误...\\\n" $cmd
552
+ t '命令输入错误...\n'
553
553
  eval usage $cmd
554
554
  ;;
555
555
  esac
@@ -33,6 +33,7 @@ const fs = __importStar(require("fs/promises"));
33
33
  const celebrate_1 = require("celebrate");
34
34
  const path_1 = require("path");
35
35
  const const_1 = require("../config/const");
36
+ const i18n_1 = require("../shared/i18n");
36
37
  const config_2 = __importDefault(require("../services/config"));
37
38
  const utils_1 = require("../shared/utils");
38
39
  const route = (0, express_1.Router)();
@@ -84,15 +85,24 @@ exports.default = (app) => {
84
85
  const logger = typedi_1.Container.get('logger');
85
86
  try {
86
87
  const { name, content } = req.body;
87
- if (config_1.default.blackFileList.includes(name)) {
88
- res.send({ code: 403, message: '文件无法访问' });
89
- }
90
- let path = (0, path_1.join)(config_1.default.configPath, name);
88
+ // Resolve path first to prevent traversal attacks
89
+ let basePath = config_1.default.configPath;
91
90
  if (name.startsWith('data/scripts/')) {
92
- path = (0, path_1.join)(config_1.default.rootPath, name);
91
+ basePath = (0, path_1.join)(config_1.default.rootPath, 'data/scripts');
92
+ }
93
+ const cleanName = name.replace(/^data\/scripts\//, '');
94
+ const resolvedPath = (0, path_1.join)(basePath, cleanName);
95
+ const normalized = (0, path_1.join)(resolvedPath);
96
+ // Verify the resolved path stays within allowed directory
97
+ if (!normalized.startsWith(basePath)) {
98
+ return res.send({ code: 403, message: (0, i18n_1.t)('文件路径无效') });
99
+ }
100
+ // Check blacklist on actual filename (not user input)
101
+ if (config_1.default.blackFileList.includes((0, path_1.basename)(normalized))) {
102
+ return res.send({ code: 403, message: (0, i18n_1.t)('文件无法访问') });
93
103
  }
94
- await (0, utils_1.writeFileWithLock)(path, content);
95
- res.send({ code: 200, message: '保存成功' });
104
+ await (0, utils_1.writeFileWithLock)(normalized, content);
105
+ res.send({ code: 200, message: (0, i18n_1.t)('保存成功') });
96
106
  }
97
107
  catch (e) {
98
108
  return next(e);
@@ -9,6 +9,8 @@ const cron_1 = __importDefault(require("../services/cron"));
9
9
  const cronView_1 = __importDefault(require("../services/cronView"));
10
10
  const celebrate_1 = require("celebrate");
11
11
  const schedule_1 = require("../validation/schedule");
12
+ const runningInstance_1 = require("../data/runningInstance");
13
+ const i18n_1 = require("../shared/i18n");
12
14
  const route = (0, express_1.Router)();
13
15
  exports.default = (app) => {
14
16
  app.use('/crons', route);
@@ -51,7 +53,7 @@ exports.default = (app) => {
51
53
  try {
52
54
  const cronViewService = typedi_1.Container.get(cronView_1.default);
53
55
  if (req.body.type === 1) {
54
- return res.send({ code: 400, message: '参数错误' });
56
+ return res.send({ code: 400, message: (0, i18n_1.t)('参数错误') });
55
57
  }
56
58
  else {
57
59
  const data = await cronViewService.update(req.body);
@@ -245,8 +247,8 @@ exports.default = (app) => {
245
247
  const logger = typedi_1.Container.get('logger');
246
248
  try {
247
249
  const cronService = typedi_1.Container.get(cron_1.default);
248
- const data = await cronService.log(req.params.id);
249
- return res.send({ code: 200, data });
250
+ const result = await cronService.log(req.params.id);
251
+ return res.send({ code: 200, data: result.content, logStatus: result.status });
250
252
  }
251
253
  catch (e) {
252
254
  return next(e);
@@ -338,6 +340,7 @@ exports.default = (app) => {
338
340
  log_path: celebrate_1.Joi.string().optional().allow(null),
339
341
  last_running_time: celebrate_1.Joi.number().optional().allow(null),
340
342
  last_execution_time: celebrate_1.Joi.number().optional().allow(null),
343
+ exit_code: celebrate_1.Joi.number().optional().allow(null),
341
344
  }),
342
345
  }), async (req, res, next) => {
343
346
  try {
@@ -349,6 +352,40 @@ exports.default = (app) => {
349
352
  return next(e);
350
353
  }
351
354
  });
355
+ route.get('/:id/instances', (0, celebrate_1.celebrate)({
356
+ params: celebrate_1.Joi.object({
357
+ id: celebrate_1.Joi.number().required(),
358
+ }),
359
+ }), async (req, res, next) => {
360
+ try {
361
+ const instances = await runningInstance_1.RunningInstanceModel.findAll({
362
+ where: {
363
+ cron_id: req.params.id,
364
+ },
365
+ order: [['started_at', 'DESC']],
366
+ raw: true,
367
+ });
368
+ return res.send({ code: 200, data: instances });
369
+ }
370
+ catch (e) {
371
+ return next(e);
372
+ }
373
+ });
374
+ route.post('/:id/instances/:instanceId/stop', (0, celebrate_1.celebrate)({
375
+ params: celebrate_1.Joi.object({
376
+ id: celebrate_1.Joi.number().required(),
377
+ instanceId: celebrate_1.Joi.number().required(),
378
+ }),
379
+ }), async (req, res, next) => {
380
+ try {
381
+ const cronService = typedi_1.Container.get(cron_1.default);
382
+ const data = await cronService.stopInstance(req.params.instanceId);
383
+ return res.send(data);
384
+ }
385
+ catch (e) {
386
+ return next(e);
387
+ }
388
+ });
352
389
  route.get('/:id/logs', (0, celebrate_1.celebrate)({
353
390
  params: celebrate_1.Joi.object({
354
391
  id: celebrate_1.Joi.number().required(),
@@ -0,0 +1,344 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = require("express");
7
+ const sequelize_1 = require("sequelize");
8
+ const cron_1 = require("../data/cron");
9
+ const cronStats_1 = require("../data/cronStats");
10
+ const runningInstance_1 = require("../data/runningInstance");
11
+ const dayjs_1 = __importDefault(require("dayjs"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const lodash_1 = require("lodash");
14
+ const i18n_1 = require("../shared/i18n");
15
+ const route = (0, express_1.Router)();
16
+ exports.default = (app) => {
17
+ app.use('/dashboard', route);
18
+ route.post('/record', async (req, res) => {
19
+ try {
20
+ const { ref_id, code, elapsed } = req.body;
21
+ if (!ref_id)
22
+ return res.send({ code: 400, message: 'ref_id required' });
23
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
24
+ const isSuccess = code === 0 ? 1 : 0;
25
+ const isFail = code !== 0 ? 1 : 0;
26
+ const elapsedMs = (Number(elapsed) || 0) * 1000;
27
+ const existing = await cronStats_1.CrontabStatModel.findOne({
28
+ where: { ref_id: Number(ref_id), date: today },
29
+ });
30
+ if (existing) {
31
+ await cronStats_1.CrontabStatModel.update({
32
+ run_count: (existing.run_count || 0) + 1,
33
+ success_count: (existing.success_count || 0) + isSuccess,
34
+ fail_count: (existing.fail_count || 0) + isFail,
35
+ total_time: (existing.total_time || 0) + elapsedMs,
36
+ max_time: Math.max(existing.max_time || 0, elapsedMs),
37
+ }, { where: { id: existing.id } });
38
+ }
39
+ else {
40
+ await cronStats_1.CrontabStatModel.create({
41
+ ref_id: Number(ref_id),
42
+ date: today,
43
+ run_count: 1,
44
+ success_count: isSuccess,
45
+ fail_count: isFail,
46
+ total_time: elapsedMs,
47
+ max_time: elapsedMs,
48
+ });
49
+ }
50
+ res.send({ code: 200 });
51
+ }
52
+ catch (e) {
53
+ res.send({ code: 500 });
54
+ }
55
+ });
56
+ route.get('/overview', async (req, res, next) => {
57
+ try {
58
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
59
+ const [total, enabled, disabled, stats] = await Promise.all([
60
+ cron_1.CrontabModel.count(),
61
+ cron_1.CrontabModel.count({ where: { isDisabled: 0 } }),
62
+ cron_1.CrontabModel.count({ where: { isDisabled: 1 } }),
63
+ cronStats_1.CrontabStatModel.findOne({
64
+ attributes: [
65
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'total_runs'],
66
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('success_count')), 'total_success'],
67
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('fail_count')), 'total_fail'],
68
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'total_time'],
69
+ ],
70
+ where: { date: today },
71
+ raw: true,
72
+ }),
73
+ ]);
74
+ const row = stats;
75
+ const totalRuns = Number(row === null || row === void 0 ? void 0 : row.total_runs) || 0;
76
+ const totalSuccess = Number(row === null || row === void 0 ? void 0 : row.total_success) || 0;
77
+ const totalFail = Number(row === null || row === void 0 ? void 0 : row.total_fail) || 0;
78
+ const totalTime = Number(row === null || row === void 0 ? void 0 : row.total_time) || 0;
79
+ res.send({
80
+ code: 200,
81
+ data: {
82
+ total,
83
+ enabled,
84
+ disabled,
85
+ todayRuns: totalRuns,
86
+ todaySuccess: totalSuccess,
87
+ todayFail: totalFail,
88
+ successRate: totalRuns > 0 ? ((totalSuccess / totalRuns) * 100).toFixed(1) : '0',
89
+ avgTime: totalRuns > 0 ? Math.round(totalTime / totalRuns) : 0,
90
+ },
91
+ });
92
+ }
93
+ catch (e) {
94
+ next(e);
95
+ }
96
+ });
97
+ route.get('/trend', async (req, res, next) => {
98
+ try {
99
+ const days = parseInt(req.query.days) || 7;
100
+ const dates = [];
101
+ for (let i = days - 1; i >= 0; i--) {
102
+ dates.push((0, dayjs_1.default)().subtract(i, 'day').format('YYYY-MM-DD'));
103
+ }
104
+ const rows = (await cronStats_1.CrontabStatModel.findAll({
105
+ attributes: [
106
+ 'date',
107
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'total_runs'],
108
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('success_count')), 'total_success'],
109
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('fail_count')), 'total_fail'],
110
+ ],
111
+ where: {
112
+ date: { [sequelize_1.Op.in]: dates },
113
+ },
114
+ group: ['date'],
115
+ order: [['date', 'ASC']],
116
+ raw: true,
117
+ }));
118
+ const dataMap = {};
119
+ rows.forEach((r) => {
120
+ dataMap[r.date] = {
121
+ total: Number(r.total_runs) || 0,
122
+ success: Number(r.total_success) || 0,
123
+ fail: Number(r.total_fail) || 0,
124
+ };
125
+ });
126
+ const data = dates.map((d) => (Object.assign({ date: (0, dayjs_1.default)(d).format('MM-DD') }, (dataMap[d] || { total: 0, success: 0, fail: 0 }))));
127
+ res.send({ code: 200, data });
128
+ }
129
+ catch (e) {
130
+ next(e);
131
+ }
132
+ });
133
+ route.get('/top-time', async (req, res, next) => {
134
+ try {
135
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
136
+ const rows = (await cronStats_1.CrontabStatModel.findAll({
137
+ attributes: [
138
+ 'ref_id',
139
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'total_time'],
140
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'run_count'],
141
+ [(0, sequelize_1.fn)('MAX', (0, sequelize_1.col)('max_time')), 'max_time'],
142
+ ],
143
+ where: { date: today, run_count: { [sequelize_1.Op.gt]: 0 } },
144
+ group: ['ref_id'],
145
+ order: [[(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'DESC']],
146
+ limit: 5,
147
+ raw: true,
148
+ }));
149
+ const ids = rows.map((r) => Number(r.ref_id));
150
+ const crons = await cron_1.CrontabModel.findAll({
151
+ where: { id: { [sequelize_1.Op.in]: ids } },
152
+ raw: true,
153
+ });
154
+ const nameMap = {};
155
+ crons.forEach((c) => { nameMap[c.id] = c.name || c.command; });
156
+ const data = rows.map((r, i) => ({
157
+ rank: i + 1,
158
+ name: nameMap[Number(r.ref_id)] || (0, i18n_1.tf)('任务#%s', r.ref_id),
159
+ avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
160
+ maxTime: Number(r.max_time),
161
+ }));
162
+ res.send({ code: 200, data });
163
+ }
164
+ catch (e) {
165
+ next(e);
166
+ }
167
+ });
168
+ route.get('/top-count', async (req, res, next) => {
169
+ try {
170
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
171
+ const rows = (await cronStats_1.CrontabStatModel.findAll({
172
+ attributes: [
173
+ 'ref_id',
174
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'run_count'],
175
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'total_time'],
176
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('success_count')), 'success_count'],
177
+ ],
178
+ where: { date: today, run_count: { [sequelize_1.Op.gt]: 0 } },
179
+ group: ['ref_id'],
180
+ order: [[(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'DESC']],
181
+ limit: 5,
182
+ raw: true,
183
+ }));
184
+ const ids = rows.map((r) => Number(r.ref_id));
185
+ const crons = await cron_1.CrontabModel.findAll({
186
+ where: { id: { [sequelize_1.Op.in]: ids } },
187
+ raw: true,
188
+ });
189
+ const nameMap = {};
190
+ crons.forEach((c) => { nameMap[c.id] = c.name || c.command; });
191
+ const data = rows.map((r, i) => ({
192
+ rank: i + 1,
193
+ name: nameMap[Number(r.ref_id)] || (0, i18n_1.tf)('任务#%s', r.ref_id),
194
+ runCount: Number(r.run_count),
195
+ avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
196
+ successRate: Number(r.run_count) > 0
197
+ ? ((Number(r.success_count) / Number(r.run_count)) * 100).toFixed(1)
198
+ : '0',
199
+ }));
200
+ res.send({ code: 200, data });
201
+ }
202
+ catch (e) {
203
+ next(e);
204
+ }
205
+ });
206
+ route.get('/runtime', async (req, res, next) => {
207
+ try {
208
+ const runningInstances = await runningInstance_1.RunningInstanceModel.findAll({
209
+ where: {
210
+ status: runningInstance_1.InstanceStatus.running,
211
+ },
212
+ raw: true,
213
+ });
214
+ const queuedCrons = await cron_1.CrontabModel.findAll({
215
+ where: {
216
+ status: 3, // queued
217
+ },
218
+ raw: true,
219
+ });
220
+ // Fetch cron names for running instances
221
+ const cronIds = [
222
+ ...new Set(runningInstances.map((i) => i.cron_id)),
223
+ ];
224
+ const crons = cronIds.length > 0
225
+ ? await cron_1.CrontabModel.findAll({
226
+ where: { id: cronIds },
227
+ raw: true,
228
+ })
229
+ : [];
230
+ const cronMap = new Map(crons.map((c) => [c.id, c]));
231
+ const now = (0, dayjs_1.default)().unix();
232
+ const running = runningInstances.map((inst) => {
233
+ const cron = cronMap.get(inst.cron_id);
234
+ return {
235
+ instanceId: inst.id,
236
+ id: inst.cron_id,
237
+ name: (cron === null || cron === void 0 ? void 0 : cron.name) || (cron === null || cron === void 0 ? void 0 : cron.command) || (0, i18n_1.tf)('任务#%s', inst.cron_id),
238
+ pid: inst.pid,
239
+ elapsed: inst.started_at ? now - inst.started_at : 0,
240
+ logPath: inst.log_path,
241
+ };
242
+ });
243
+ const dayAgo = (0, dayjs_1.default)().subtract(24, 'hour').unix();
244
+ const idleTasks = await cron_1.CrontabModel.findAll({
245
+ where: {
246
+ isDisabled: 0,
247
+ status: 1,
248
+ last_execution_time: { [sequelize_1.Op.lt]: dayAgo },
249
+ },
250
+ order: [['last_execution_time', 'ASC']],
251
+ limit: 5,
252
+ raw: true,
253
+ });
254
+ res.send({
255
+ code: 200,
256
+ data: {
257
+ runningCount: running.length,
258
+ queuedCount: queuedCrons.length,
259
+ running,
260
+ idleTasks: idleTasks.map((c) => ({
261
+ id: c.id,
262
+ name: c.name || c.command || (0, i18n_1.tf)('任务#%s', c.id),
263
+ lastRun: c.last_execution_time
264
+ ? dayjs_1.default.unix(c.last_execution_time).format('MM-DD HH:mm')
265
+ : '-',
266
+ })),
267
+ },
268
+ });
269
+ }
270
+ catch (e) {
271
+ next(e);
272
+ }
273
+ });
274
+ route.get('/labels', async (req, res, next) => {
275
+ try {
276
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
277
+ const [crons, stats] = (await Promise.all([
278
+ cron_1.CrontabModel.findAll({ where: { isDisabled: 0 }, raw: true }),
279
+ cronStats_1.CrontabStatModel.findAll({ where: { date: today }, raw: true }),
280
+ ]));
281
+ const statMap = {};
282
+ stats.forEach((s) => { statMap[s.ref_id] = s; });
283
+ const labelMap = {};
284
+ crons.forEach((c) => {
285
+ let rawLabels = c.labels;
286
+ if (typeof rawLabels === 'string')
287
+ rawLabels = JSON.parse(rawLabels);
288
+ const labels = Array.isArray(rawLabels)
289
+ ? [...new Set(rawLabels.filter((l) => !(0, lodash_1.isEmpty)(l)))]
290
+ : [];
291
+ if (labels.length === 0) {
292
+ labels.push((0, i18n_1.t)('未分类'));
293
+ }
294
+ const st = statMap[c.id];
295
+ labels.forEach((label) => {
296
+ if (!labelMap[label])
297
+ labelMap[label] = { count: 0, runs: 0, success: 0, totalTime: 0 };
298
+ labelMap[label].count += 1;
299
+ if (st) {
300
+ labelMap[label].runs += Number(st.run_count) || 0;
301
+ labelMap[label].success += Number(st.success_count) || 0;
302
+ labelMap[label].totalTime += Number(st.total_time) || 0;
303
+ }
304
+ });
305
+ });
306
+ const data = Object.entries(labelMap)
307
+ .map(([label, v]) => ({
308
+ label,
309
+ count: v.count,
310
+ todayRuns: v.runs,
311
+ successRate: v.runs > 0 ? ((v.success / v.runs) * 100).toFixed(1) : '0',
312
+ avgTime: v.runs > 0 ? Math.round(v.totalTime / v.runs) : 0,
313
+ }))
314
+ .sort((a, b) => b.todayRuns - a.todayRuns);
315
+ res.send({ code: 200, data });
316
+ }
317
+ catch (e) {
318
+ next(e);
319
+ }
320
+ });
321
+ route.get('/system', async (req, res, next) => {
322
+ try {
323
+ const memUsage = process.memoryUsage();
324
+ res.send({
325
+ code: 200,
326
+ data: {
327
+ platform: os_1.default.platform(),
328
+ uptime: Math.floor(process.uptime()),
329
+ memTotal: os_1.default.totalmem(),
330
+ memFree: os_1.default.freemem(),
331
+ memUsagePercent: ((1 - os_1.default.freemem() / os_1.default.totalmem()) * 100).toFixed(1),
332
+ heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
333
+ heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
334
+ loadAvg: os_1.default.loadavg().map((v) => Number(v.toFixed(2))),
335
+ cpus: os_1.default.cpus().length,
336
+ },
337
+ });
338
+ }
339
+ catch (e) {
340
+ next(e);
341
+ }
342
+ });
343
+ };
344
+ //# sourceMappingURL=dashboard.js.map