good_job 2.12.1 → 2.13.1

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/engine/app/assets/modules/application.js +12 -0
  4. data/engine/app/assets/modules/charts.js +29 -0
  5. data/engine/app/assets/modules/document_ready.js +7 -0
  6. data/engine/app/assets/modules/poller.js +93 -0
  7. data/engine/app/assets/modules/toasts.js +8 -0
  8. data/engine/app/assets/scripts.js +1 -131
  9. data/engine/app/assets/style.css +9 -3
  10. data/engine/app/assets/vendor/es_module_shims.js +1 -0
  11. data/engine/app/controllers/good_job/assets_controller.rb +17 -0
  12. data/engine/app/controllers/good_job/executions_controller.rb +1 -5
  13. data/engine/app/controllers/good_job/jobs_controller.rb +1 -1
  14. data/engine/app/views/good_job/cron_entries/show.html.erb +4 -2
  15. data/engine/app/views/good_job/executions/_table.erb +2 -2
  16. data/engine/app/views/good_job/jobs/_table.erb +2 -2
  17. data/engine/app/views/good_job/jobs/index.html.erb +2 -10
  18. data/engine/app/views/good_job/shared/_alert.erb +20 -13
  19. data/engine/app/views/good_job/shared/_chart.erb +4 -2
  20. data/engine/app/views/good_job/shared/_filter.erb +58 -50
  21. data/engine/app/views/good_job/shared/_footer.erb +2 -4
  22. data/engine/app/views/good_job/shared/_navbar.erb +5 -8
  23. data/engine/app/views/layouts/good_job/application.html.erb +17 -12
  24. data/engine/config/locales/en.yml +2 -3
  25. data/engine/config/locales/es.yml +1 -2
  26. data/engine/config/locales/nl.yml +52 -0
  27. data/engine/config/locales/ru.yml +76 -0
  28. data/engine/config/routes.rb +5 -3
  29. data/lib/good_job/adapter.rb +2 -2
  30. data/lib/good_job/version.rb +1 -1
  31. metadata +10 -5
  32. data/engine/app/filters/good_job/executions_filter.rb +0 -41
  33. data/engine/app/views/good_job/executions/index.html.erb +0 -19
  34. data/engine/config/locales/ru.yaml +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53c32b60d9ee966dd7dcb05f5add8f3ab7bcd666a224019d91b0e07e54a366b4
4
- data.tar.gz: 7abcb16ceb51272d9fc6297491c55500b97a2839c6158c24d9779841b179c721
3
+ metadata.gz: 6d8b2b2738eb9ae693690016074fd4624a5c5cc78abd5d87d416202e5671281e
4
+ data.tar.gz: dbb67bc97bdeca64de1a6bb9b7c1fd16bf51a5fa048bf1d87ca637860d6e80eb
5
5
  SHA512:
6
- metadata.gz: a65fc3d71709b9c4ee611f79e3678b9ef910d8480757037fd25f83f9d783eff40134b0b15fad873d31fb60c576872a93fd64bc2c7f0f2ba9b4ce0d751984925c
7
- data.tar.gz: eea513879d7cdbb872938469a62672d886156a92f63a61c88e8a62c816a218bc325be1b705d1e1f7f7a43626ba2910bd108ea14303b6a15b59674a85eddf1f58
6
+ metadata.gz: 4bb91a8ab93b89c82084f58198d6dcdbcab5e44e3b42cdf6eb2f94fdd895bf126d146cfd6e314ea69830ed0013dd66796c727c2a2b48e0a7456089252b987faa
7
+ data.tar.gz: 6b86d012b1deb3c71f5cfd4bb69ac680eee1b18cb35849d24adc0ad8ef2b4689c217b39984b4bf7af3075395bf3464d0c1fd1198e7f67c083b1ddcbd32950bb5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.13.1](https://github.com/bensheldon/good_job/tree/v2.13.1) (2022-04-22)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.13.0...v2.13.1)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - `ActionMailer::MailDeliveryJob` executing twice [\#329](https://github.com/bensheldon/good_job/issues/329)
10
+ - Email job breaks dashboard [\#313](https://github.com/bensheldon/good_job/issues/313)
11
+
12
+ **Closed issues:**
13
+
14
+ - Possible encryption feature? [\#561](https://github.com/bensheldon/good_job/issues/561)
15
+ - Inconsistencies in configuration settings [\#380](https://github.com/bensheldon/good_job/issues/380)
16
+ - Lockable should accept an explicit keys on class methods too [\#341](https://github.com/bensheldon/good_job/issues/341)
17
+ - Run Scheduler\#cache\_warm on global thread pool instead of Scheduler's thread pool [\#286](https://github.com/bensheldon/good_job/issues/286)
18
+
19
+ **Merged pull requests:**
20
+
21
+ - Dashboard: Use toasts to show notices and alerts [\#577](https://github.com/bensheldon/good_job/pull/577) ([bkeepers](https://github.com/bkeepers))
22
+ - Remove executions from the dashboard [\#576](https://github.com/bensheldon/good_job/pull/576) ([bkeepers](https://github.com/bkeepers))
23
+ - Use javascript importmaps for Dashboard [\#574](https://github.com/bensheldon/good_job/pull/574) ([bensheldon](https://github.com/bensheldon))
24
+
25
+ ## [v2.13.0](https://github.com/bensheldon/good_job/tree/v2.13.0) (2022-04-19)
26
+
27
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.12.2...v2.13.0)
28
+
29
+ **Implemented enhancements:**
30
+
31
+ - Dashboard UI updates: sticky navbar, statuses as tabs [\#572](https://github.com/bensheldon/good_job/pull/572) ([bkeepers](https://github.com/bkeepers))
32
+
33
+ **Closed issues:**
34
+
35
+ - Internationalize/I18n the Dashboard Engine [\#408](https://github.com/bensheldon/good_job/issues/408)
36
+
37
+ **Merged pull requests:**
38
+
39
+ - Fix Russian translation linting [\#573](https://github.com/bensheldon/good_job/pull/573) ([bensheldon](https://github.com/bensheldon))
40
+
41
+ ## [v2.12.2](https://github.com/bensheldon/good_job/tree/v2.12.2) (2022-04-18)
42
+
43
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.12.1...v2.12.2)
44
+
45
+ **Merged pull requests:**
46
+
47
+ - Dashboard: added NL translations [\#568](https://github.com/bensheldon/good_job/pull/568) ([eelcoj](https://github.com/eelcoj))
48
+ - Un-deprecate Adapter's `execution_mode` argument [\#567](https://github.com/bensheldon/good_job/pull/567) ([bensheldon](https://github.com/bensheldon))
49
+
3
50
  ## [v2.12.1](https://github.com/bensheldon/good_job/tree/v2.12.1) (2022-04-18)
4
51
 
5
52
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.12.0...v2.12.1)
@@ -0,0 +1,12 @@
1
+ /*jshint esversion: 6, strict: false */
2
+
3
+ import documentReady from "document_ready";
4
+ import showToasts from "toasts";
5
+ import renderCharts from "charts";
6
+ import Poller from "poller";
7
+
8
+ documentReady(function() {
9
+ renderCharts();
10
+ showToasts();
11
+ Poller.start();
12
+ });
@@ -0,0 +1,29 @@
1
+ function renderCharts(animate) {
2
+ const charts = document.querySelectorAll('.chart');
3
+
4
+ for (let i = 0; i < charts.length; i++) {
5
+ const chartEl = charts[i];
6
+ const chartData = JSON.parse(chartEl.dataset.json);
7
+
8
+ const ctx = chartEl.getContext('2d');
9
+ const chart = new Chart(ctx, {
10
+ type: 'line',
11
+ data: {
12
+ labels: chartData.labels,
13
+ datasets: chartData.datasets
14
+ },
15
+ options: {
16
+ animation: animate,
17
+ responsive: true,
18
+ maintainAspectRatio: false,
19
+ scales: {
20
+ y: {
21
+ beginAtZero: true
22
+ }
23
+ }
24
+ }
25
+ });
26
+ }
27
+ }
28
+
29
+ export { renderCharts as default };
@@ -0,0 +1,7 @@
1
+ export default function documentReady(callback) {
2
+ if (document.readyState !== "loading") {
3
+ callback();
4
+ } else {
5
+ document.addEventListener("DOMContentLoaded", callback);
6
+ }
7
+ }
@@ -0,0 +1,93 @@
1
+ /*jshint esversion: 6, strict: false */
2
+ import renderCharts from "charts";
3
+
4
+ // NOTE: this file is a bit disorganized. Please do not use it as a template for how to organize a JS module.
5
+
6
+ const DEFAULT_POLL_INTERVAL_SECONDS = 30;
7
+ const MINIMUM_POLL_INTERVAL = 1000;
8
+
9
+ function getStorage(key) {
10
+ const value = localStorage.getItem('good_job-' + key);
11
+
12
+ if (value === 'true') {
13
+ return true;
14
+ } else if (value === 'false') {
15
+ return false;
16
+ } else {
17
+ return value;
18
+ }
19
+ }
20
+
21
+ function setStorage(key, value) {
22
+ localStorage.setItem('good_job-' + key, value);
23
+ }
24
+
25
+ function updatePageContent(newContent) {
26
+ const domParser = new DOMParser();
27
+ const parsedDOM = domParser.parseFromString(newContent, "text/html");
28
+
29
+ const newElements = parsedDOM.querySelectorAll('[data-gj-poll-replace]');
30
+
31
+ for (let i = 0; i < newElements.length; i++) {
32
+ const newEl = newElements[i];
33
+ const oldEl = document.getElementById(newEl.id);
34
+
35
+ if (oldEl) {
36
+ oldEl.replaceWith(newEl);
37
+ }
38
+ }
39
+
40
+ renderCharts(false);
41
+ }
42
+
43
+ function refreshPage() {
44
+ fetch(window.location.href)
45
+ .then(resp => resp.text())
46
+ .then(updatePageContent);
47
+ }
48
+
49
+ const Poller = {
50
+ start: () => {
51
+ Poller.updateSettings();
52
+ Poller.pollUpdates();
53
+
54
+ const checkbox = document.querySelector('input[name="toggle-poll"]');
55
+ checkbox.addEventListener('change', Poller.togglePoll)
56
+ },
57
+
58
+ togglePoll: (event) => {
59
+ Poller.pollEnabled = event.currentTarget.checked;
60
+ setStorage('pollEnabled', Poller.pollEnabled);
61
+ },
62
+
63
+ updateSettings: () => {
64
+ const queryString = window.location.search;
65
+ const urlParams = new URLSearchParams(queryString);
66
+
67
+ if (urlParams.has('poll')) {
68
+ const parsedInterval = (parseInt(urlParams.get('poll')) || DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
69
+ Poller.pollInterval = Math.max(parsedInterval, MINIMUM_POLL_INTERVAL);
70
+ setStorage('pollInterval', Poller.pollInterval);
71
+
72
+ Poller.pollEnabled = true;
73
+ } else {
74
+ Poller.pollInterval = getStorage('pollInterval') || (DEFAULT_POLL_INTERVAL_SECONDS * 1000);
75
+ Poller.pollEnabled = getStorage('pollEnabled') || false;
76
+ }
77
+
78
+ document.getElementById('toggle-poll').checked = Poller.pollEnabled;
79
+ },
80
+
81
+ pollUpdates: () => {
82
+ setTimeout(() => {
83
+ if (Poller.pollEnabled === true) {
84
+ refreshPage();
85
+ Poller.pollUpdates();
86
+ } else {
87
+ Poller.pollUpdates();
88
+ }
89
+ }, Poller.pollInterval);
90
+ },
91
+ };
92
+
93
+ export { Poller as default };
@@ -0,0 +1,8 @@
1
+ export default function showToasts() {
2
+ const toasts = document.querySelectorAll('.toast');
3
+
4
+ for (let i = 0; i < toasts.length; i++) {
5
+ var toast = new bootstrap.Toast(toasts[i])
6
+ toast.show()
7
+ }
8
+ }
@@ -1,133 +1,3 @@
1
1
  /*jshint esversion: 6, strict: false */
2
- const GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS = 30;
3
- const GOOD_JOB_MINIMUM_POLL_INTERVAL = 1000;
4
2
 
5
- const GoodJob = {
6
- // Register functions to execute when the DOM is ready
7
- ready: (callback) => {
8
- if (document.readyState !== "loading") {
9
- callback();
10
- } else {
11
- document.addEventListener("DOMContentLoaded", callback);
12
- }
13
- },
14
-
15
- init: () => {
16
- GoodJob.updateSettings();
17
- GoodJob.addListeners();
18
- GoodJob.pollUpdates();
19
- GoodJob.renderCharts(true);
20
- },
21
-
22
- addListeners: () => {
23
- const gjActionEls = document.querySelectorAll('[data-gj-action]');
24
-
25
- for (let i = 0; i < gjActionEls.length; i++) {
26
- const el = gjActionEls[i];
27
- const [eventName, func] = el.dataset.gjAction.split('#');
28
-
29
- el.addEventListener(eventName, GoodJob[func]);
30
- }
31
- },
32
-
33
- updateSettings: () => {
34
- const queryString = window.location.search;
35
- const urlParams = new URLSearchParams(queryString);
36
-
37
- // live poll interval and enablement
38
- if (urlParams.has('poll')) {
39
- const parsedInterval = (parseInt(urlParams.get('poll')) || GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
40
- GoodJob.pollInterval = Math.max(parsedInterval, GOOD_JOB_MINIMUM_POLL_INTERVAL);
41
- GoodJob.setStorage('pollInterval', GoodJob.pollInterval);
42
-
43
- GoodJob.pollEnabled = true;
44
- } else {
45
- GoodJob.pollInterval = GoodJob.getStorage('pollInterval') || (GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS * 1000);
46
- GoodJob.pollEnabled = GoodJob.getStorage('pollEnabled') || false;
47
- }
48
-
49
- document.getElementById('toggle-poll').checked = GoodJob.pollEnabled;
50
- },
51
-
52
- togglePoll: (ev) => {
53
- GoodJob.pollEnabled = ev.currentTarget.checked;
54
- GoodJob.setStorage('pollEnabled', GoodJob.pollEnabled);
55
- },
56
-
57
- pollUpdates: () => {
58
- setTimeout(() => {
59
- if (GoodJob.pollEnabled === true) {
60
- fetch(window.location.href)
61
- .then(resp => resp.text())
62
- .then(GoodJob.updateContent)
63
- .finally(GoodJob.pollUpdates);
64
- } else {
65
- GoodJob.pollUpdates();
66
- }
67
- }, GoodJob.pollInterval);
68
- },
69
-
70
- updateContent: (newContent) => {
71
- const domParser = new DOMParser();
72
- const parsedDOM = domParser.parseFromString(newContent, "text/html");
73
-
74
- const newElements = parsedDOM.querySelectorAll('[data-gj-poll-replace]');
75
-
76
- for (let i = 0; i < newElements.length; i++) {
77
- const newEl = newElements[i];
78
- const oldEl = document.getElementById(newEl.id);
79
-
80
- if (oldEl) {
81
- oldEl.replaceWith(newEl);
82
- }
83
- }
84
-
85
- GoodJob.renderCharts(false);
86
- },
87
-
88
- renderCharts: (animate) => {
89
- const charts = document.querySelectorAll('.chart');
90
-
91
- for (let i = 0; i < charts.length; i++) {
92
- const chartEl = charts[i];
93
- const chartData = JSON.parse(chartEl.dataset.json);
94
-
95
- const ctx = chartEl.getContext('2d');
96
- const chart = new Chart(ctx, {
97
- type: 'line',
98
- data: {
99
- labels: chartData.labels,
100
- datasets: chartData.datasets
101
- },
102
- options: {
103
- animation: animate,
104
- responsive: true,
105
- maintainAspectRatio: false,
106
- scales: {
107
- y: {
108
- beginAtZero: true
109
- }
110
- }
111
- }
112
- });
113
- }
114
- },
115
-
116
- getStorage: (key) => {
117
- const value = localStorage.getItem('good_job-' + key);
118
-
119
- if (value === 'true') {
120
- return true;
121
- } else if (value === 'false') {
122
- return false;
123
- } else {
124
- return value;
125
- }
126
- },
127
-
128
- setStorage: (key, value) => {
129
- localStorage.setItem('good_job-' + key, value);
130
- }
131
- };
132
-
133
- GoodJob.ready(GoodJob.init);
3
+ import "application"; // ./modules/application.js
@@ -24,7 +24,13 @@
24
24
  height: 200px;
25
25
  }
26
26
 
27
- body {
28
- /* Make room for the sticky footer */
29
- margin-bottom: 100px;
27
+ /* Break out of a container */
28
+ .break-out {
29
+ width:100vw;
30
+ position:relative;
31
+ left:calc(-1 * (100vw - 100%)/2);
32
+ }
33
+
34
+ .toast-container {
35
+ z-index: 1;
30
36
  }
@@ -0,0 +1 @@
1
+ (function(){const noop=()=>{};const e=document.querySelector("script[type=esms-options]");const t=e?JSON.parse(e.innerHTML):{};Object.assign(t,self.esmsInitOptions||{});let r=!!t.shimMode;const s=globalHook(r&&t.onimport);const a=globalHook(r&&t.resolve);let n=t.fetch?globalHook(t.fetch):fetch;const i=t.meta?globalHook(shimModule&&t.meta):noop;const c=t.skip?new RegExp(t.skip):null;let f=t.nonce;const te=t.mapOverrides;if(!f){const e=document.querySelector("script[nonce]");e&&(f=e.nonce||e.getAttribute("nonce"))}const re=globalHook(t.onerror||noop);const se=t.onpolyfill?globalHook(t.onpolyfill):()=>console.log("%c^^ Module TypeError above is polyfilled and can be ignored ^^","font-weight:900;color:#391");const{revokeBlobURLs:ae,noLoadEventRetriggers:ne,enforceIntegrity:ie}=t;function globalHook(e){return"string"===typeof e?self[e]:e}const oe=Array.isArray(t.polyfillEnable)?t.polyfillEnable:[];const ce=oe.includes("css-modules");const le=oe.includes("json-modules");function setShimMode(){r=true}const fe=!navigator.userAgentData&&!!navigator.userAgent.match(/Edge\/\d+\.\d+/);const ue=document.baseURI;function createBlob(e,t="text/javascript"){return URL.createObjectURL(new Blob([e],{type:t}))}const eoop=e=>setTimeout((()=>{throw e}));const throwError=e=>{(window.reportError||window.safari&&console.error||eoop)(e),void re(e)};function fromParent(e){return e?` imported from ${e}`:""}const de=/\\/g;function isURL(e){if(-1===e.indexOf(":"))return false;try{new URL(e);return true}catch(e){return false}}function resolveUrl(e,t){return resolveIfNotPlainOrUrl(e,t)||(isURL(e)?e:resolveIfNotPlainOrUrl("./"+e,t))}function resolveIfNotPlainOrUrl(e,t){const r=t.indexOf("?",-1===t.indexOf("#")?t.indexOf("#"):t.length);-1!==r&&(t=t.slice(0,r));-1!==e.indexOf("\\")&&(e=e.replace(de,"/"));if("/"===e[0]&&"/"===e[1])return t.slice(0,t.indexOf(":")+1)+e;if("."===e[0]&&("/"===e[1]||"."===e[1]&&("/"===e[2]||2===e.length&&(e+="/"))||1===e.length&&(e+="/"))||"/"===e[0]){const r=t.slice(0,t.indexOf(":")+1);let s;if("/"===t[r.length+1])if("file:"!==r){s=t.slice(r.length+2);s=s.slice(s.indexOf("/")+1)}else s=t.slice(8);else s=t.slice(r.length+("/"===t[r.length]));if("/"===e[0])return t.slice(0,t.length-s.length-1)+e;const a=s.slice(0,s.lastIndexOf("/")+1)+e;const n=[];let i=-1;for(let e=0;e<a.length;e++)if(-1===i){if("."===a[e]){if("."===a[e+1]&&("/"===a[e+2]||e+2===a.length)){n.pop();e+=2;continue}if("/"===a[e+1]||e+1===a.length){e+=1;continue}}while("/"===a[e])e++;i=e}else if("/"===a[e]){n.push(a.slice(i,e+1));i=-1}-1!==i&&n.push(a.slice(i));return t.slice(0,t.length-s.length)+n.join("")}}function resolveAndComposeImportMap(e,t,r){const s={imports:Object.assign({},r.imports),scopes:Object.assign({},r.scopes)};e.imports&&resolveAndComposePackages(e.imports,s.imports,t,r);if(e.scopes)for(let a in e.scopes){const n=resolveUrl(a,t);resolveAndComposePackages(e.scopes[a],s.scopes[n]||(s.scopes[n]={}),t,r)}return s}function getMatch(e,t){if(t[e])return e;let r=e.length;do{const s=e.slice(0,r+1);if(s in t)return s}while(-1!==(r=e.lastIndexOf("/",r-1)))}function applyPackages(e,t){const r=getMatch(e,t);if(r){const s=t[r];if(null===s)return;return s+e.slice(r.length)}}function resolveImportMap(e,t,r){let s=r&&getMatch(r,e.scopes);while(s){const r=applyPackages(t,e.scopes[s]);if(r)return r;s=getMatch(s.slice(0,s.lastIndexOf("/")),e.scopes)}return applyPackages(t,e.imports)||-1!==t.indexOf(":")&&t}function resolveAndComposePackages(e,t,s,a){for(let n in e){const i=resolveIfNotPlainOrUrl(n,s)||n;if((!r||!te)&&t[i]&&t[i]!==e[i])throw Error(`Rejected map override "${i}" from ${t[i]} to ${e[i]}.`);let c=e[n];if("string"!==typeof c)continue;const f=resolveImportMap(a,resolveIfNotPlainOrUrl(c,s)||c,s);f?t[i]=f:console.warn(`Mapping "${n}" -> "${e[n]}" does not resolve`)}}let pe;window.addEventListener("error",(e=>pe=e));function dynamicImportScript(e,{errUrl:t=e}={}){pe=void 0;const r=createBlob(`import*as m from'${e}';self._esmsi=m`);const s=Object.assign(document.createElement("script"),{type:"module",src:r});s.setAttribute("nonce",f);s.setAttribute("noshim","");const a=new Promise(((e,a)=>{s.addEventListener("error",cb);s.addEventListener("load",cb);function cb(n){document.head.removeChild(s);if(self._esmsi){e(self._esmsi,ue);self._esmsi=void 0}else{a(!(n instanceof Event)&&n||pe&&pe.error||new Error(`Error loading or executing the graph of ${t} (check the console for ${r}).`));pe=void 0}}}));document.head.appendChild(s);return a}let be=dynamicImportScript;const he=dynamicImportScript(createBlob("export default u=>import(u)")).then((e=>{e&&(be=e.default);return!!e}),noop);let ke=false;let me=false;let we=false;let ge=false;let ve=false;const ye=Promise.resolve(he).then((e=>{if(e){ve=true;return Promise.all([be(createBlob("import.meta")).then((()=>we=true),noop),ce&&be(createBlob('import"data:text/css,{}"assert{type:"css"}')).then((()=>me=true),noop),le&&be(createBlob('import"data:text/json,{}"assert{type:"json"}')).then((()=>ke=true),noop),new Promise((e=>{self._$s=r=>{document.head.removeChild(t);r&&(ge=true);delete self._$s;e()};const t=document.createElement("iframe");t.style.display="none";t.srcdoc=`<script type=importmap nonce="${f}">{"imports":{"x":"data:text/javascript,"}}<\/script><script nonce="${f}">import('x').then(()=>1,()=>0).then(v=>parent._$s(v))<\/script>`;document.head.appendChild(t)}))])}}));let Se,$e,Le,Oe=2<<19;const Ce=1===new Uint8Array(new Uint16Array([1]).buffer)[0]?function(e,t){const r=e.length;let s=0;for(;s<r;)t[s]=e.charCodeAt(s++)}:function(e,t){const r=e.length;let s=0;for(;s<r;){const r=e.charCodeAt(s);t[s++]=(255&r)<<8|r>>>8}},Ae="xportmportlassetafromssertvoyiedeleinstantyreturdebuggeawaithrwhileforifcatcfinallels";let Ue,Ie,Me;function parse(e,t="@"){Ue=e,Ie=t;const r=2*Ue.length+(2<<18);if(r>Oe||!Se){for(;r>Oe;)Oe*=2;$e=new ArrayBuffer(Oe),Ce(Ae,new Uint16Array($e,16,85)),Se=function(e,t,r){"use asm";var s=new e.Int8Array(r),a=new e.Int16Array(r),n=new e.Int32Array(r),i=new e.Uint8Array(r),c=new e.Uint16Array(r),f=992;function b(e){e=e|0;var t=0,r=0,i=0,te=0,re=0,se=0,ae=0;ae=f;f=f+11520|0;re=ae+2048|0;s[763]=1;a[377]=0;a[378]=0;a[379]=0;a[380]=-1;n[57]=n[2];s[764]=0;n[56]=0;s[762]=0;n[58]=ae+10496;n[59]=ae+2304;n[60]=ae;s[765]=0;e=(n[3]|0)+-2|0;n[61]=e;t=e+(n[54]<<1)|0;n[62]=t;e:while(1){r=e+2|0;n[61]=r;if(e>>>0>=t>>>0){te=18;break}t:do{switch(a[r>>1]|0){case 9:case 10:case 11:case 12:case 13:case 32:break;case 101:{if((((a[379]|0)==0?D(r)|0:0)?(m(e+4|0,16,10)|0)==0:0)?(k(),(s[763]|0)==0):0){te=9;break e}else te=17;break}case 105:{if(D(r)|0?(m(e+4|0,26,10)|0)==0:0){l();te=17}else te=17;break}case 59:{te=17;break}case 47:switch(a[e+4>>1]|0){case 47:{j();break t}case 42:{y(1);break t}default:{te=16;break e}}default:{te=16;break e}}}while(0);if((te|0)==17){te=0;n[57]=n[61]}e=n[61]|0;t=n[62]|0}if((te|0)==9){e=n[61]|0;n[57]=e;te=19}else if((te|0)==16){s[763]=0;n[61]=e;te=19}else if((te|0)==18)if(!(s[762]|0)){e=r;te=19}else e=0;do{if((te|0)==19){e:while(1){t=e+2|0;n[61]=t;i=t;if(e>>>0>=(n[62]|0)>>>0){te=75;break}t:do{switch(a[t>>1]|0){case 9:case 10:case 11:case 12:case 13:case 32:break;case 101:{if(((a[379]|0)==0?D(t)|0:0)?(m(e+4|0,16,10)|0)==0:0){k();te=74}else te=74;break}case 105:{if(D(t)|0?(m(e+4|0,26,10)|0)==0:0){l();te=74}else te=74;break}case 99:{if((D(t)|0?(m(e+4|0,36,8)|0)==0:0)?M(a[e+12>>1]|0)|0:0){s[765]=1;te=74}else te=74;break}case 40:{r=n[57]|0;i=n[59]|0;te=a[379]|0;a[379]=te+1<<16>>16;n[i+((te&65535)<<2)>>2]=r;te=74;break}case 41:{t=a[379]|0;if(!(t<<16>>16)){te=36;break e}t=t+-1<<16>>16;a[379]=t;r=a[378]|0;if(r<<16>>16!=0?(se=n[(n[60]|0)+((r&65535)+-1<<2)>>2]|0,(n[se+20>>2]|0)==(n[(n[59]|0)+((t&65535)<<2)>>2]|0)):0){t=se+4|0;if(!(n[t>>2]|0))n[t>>2]=i;n[se+12>>2]=e+4;a[378]=r+-1<<16>>16;te=74}else te=74;break}case 123:{te=n[57]|0;i=n[51]|0;e=te;do{if((a[te>>1]|0)==41&(i|0)!=0?(n[i+4>>2]|0)==(te|0):0){t=n[52]|0;n[51]=t;if(!t){n[47]=0;break}else{n[t+28>>2]=0;break}}}while(0);r=a[379]|0;te=r&65535;s[re+te>>0]=s[765]|0;s[765]=0;i=n[59]|0;a[379]=r+1<<16>>16;n[i+(te<<2)>>2]=e;te=74;break}case 125:{e=a[379]|0;if(!(e<<16>>16)){te=49;break e}r=e+-1<<16>>16;a[379]=r;t=a[380]|0;if(e<<16>>16!=t<<16>>16)if(t<<16>>16!=-1&(r&65535)<(t&65535)){te=53;break e}else{te=74;break t}else{i=n[58]|0;te=(a[377]|0)+-1<<16>>16;a[377]=te;a[380]=a[i+((te&65535)<<1)>>1]|0;h();te=74;break t}}case 39:{d(39);te=74;break}case 34:{d(34);te=74;break}case 47:switch(a[e+4>>1]|0){case 47:{j();break t}case 42:{y(1);break t}default:{t=n[57]|0;r=a[t>>1]|0;r:do{if(!(U(r)|0)){switch(r<<16>>16){case 41:if(q(n[(n[59]|0)+(c[379]<<2)>>2]|0)|0){te=71;break r}else{te=68;break r}case 125:break;default:{te=68;break r}}e=c[379]|0;if(!(p(n[(n[59]|0)+(e<<2)>>2]|0)|0)?(s[re+e>>0]|0)==0:0)te=68;else te=71}else switch(r<<16>>16){case 46:if(((a[t+-2>>1]|0)+-48&65535)<10){te=68;break r}else{te=71;break r}case 43:if((a[t+-2>>1]|0)==43){te=68;break r}else{te=71;break r}case 45:if((a[t+-2>>1]|0)==45){te=68;break r}else{te=71;break r}default:{te=71;break r}}}while(0);r:do{if((te|0)==68){te=0;if(!(o(t)|0)){switch(r<<16>>16){case 0:{te=71;break r}case 47:break;default:{e=1;break r}}if(!(s[764]|0))e=1;else te=71}else te=71}}while(0);if((te|0)==71){g();e=0}s[764]=e;te=74;break t}}case 96:{h();te=74;break}default:te=74}}while(0);if((te|0)==74){te=0;n[57]=n[61]}e=n[61]|0}if((te|0)==36){L();e=0;break}else if((te|0)==49){L();e=0;break}else if((te|0)==53){L();e=0;break}else if((te|0)==75){e=(a[380]|0)==-1&(a[379]|0)==0&(s[762]|0)==0&(a[378]|0)==0;break}}}while(0);f=ae;return e|0}function k(){var e=0,t=0,r=0,i=0,c=0,f=0;c=n[61]|0;f=c+12|0;n[61]=f;t=w(1)|0;e=n[61]|0;if(!((e|0)==(f|0)?!(I(t)|0):0))i=3;e:do{if((i|0)==3){t:do{switch(t<<16>>16){case 100:{B(e,e+14|0);break e}case 97:{n[61]=e+10;w(1)|0;e=n[61]|0;i=6;break}case 102:{i=6;break}case 99:{if((m(e+2|0,36,8)|0)==0?(r=e+10|0,$(a[r>>1]|0)|0):0){n[61]=r;c=w(1)|0;f=n[61]|0;E(c)|0;B(f,n[61]|0);n[61]=(n[61]|0)+-2;break e}e=e+4|0;n[61]=e;i=13;break}case 108:case 118:{i=13;break}case 123:{n[61]=e+2;e=w(1)|0;r=n[61]|0;while(1){if(N(e)|0){d(e);e=(n[61]|0)+2|0;n[61]=e}else{E(e)|0;e=n[61]|0}w(1)|0;e=C(r,e)|0;if(e<<16>>16==44){n[61]=(n[61]|0)+2;e=w(1)|0}t=r;r=n[61]|0;if(e<<16>>16==125){i=32;break}if((r|0)==(t|0)){i=29;break}if(r>>>0>(n[62]|0)>>>0){i=31;break}}if((i|0)==29){L();break e}else if((i|0)==31){L();break e}else if((i|0)==32){n[61]=r+2;i=34;break t}break}case 42:{n[61]=e+2;w(1)|0;i=n[61]|0;C(i,i)|0;i=34;break}default:{}}}while(0);if((i|0)==6){n[61]=e+16;e=w(1)|0;if(e<<16>>16==42){n[61]=(n[61]|0)+2;e=w(1)|0}f=n[61]|0;E(e)|0;B(f,n[61]|0);n[61]=(n[61]|0)+-2;break}else if((i|0)==13){e=e+4|0;n[61]=e;s[763]=0;t:while(1){n[61]=e+2;f=w(1)|0;e=n[61]|0;switch((E(f)|0)<<16>>16){case 91:case 123:{i=15;break t}default:{}}t=n[61]|0;if((t|0)==(e|0))break e;B(e,t);switch((w(1)|0)<<16>>16){case 61:{i=19;break t}case 44:break;default:{i=20;break t}}e=n[61]|0}if((i|0)==15){n[61]=(n[61]|0)+-2;break}else if((i|0)==19){n[61]=(n[61]|0)+-2;break}else if((i|0)==20){n[61]=(n[61]|0)+-2;break}}else if((i|0)==34)t=w(1)|0;e=n[61]|0;if(t<<16>>16==102?(m(e+2|0,52,6)|0)==0:0){n[61]=e+8;u(c,w(1)|0);break}n[61]=e+-2}}while(0);return}function l(){var e=0,t=0,r=0,i=0,c=0;c=n[61]|0;t=c+12|0;n[61]=t;e:do{switch((w(1)|0)<<16>>16){case 40:{e=n[61]|0;t=n[59]|0;r=a[379]|0;a[379]=r+1<<16>>16;n[t+((r&65535)<<2)>>2]=e;if((a[n[57]>>1]|0)!=46){e=n[61]|0;n[61]=e+2;r=w(1)|0;v(c,n[61]|0,0,e);e=n[51]|0;t=n[60]|0;c=a[378]|0;a[378]=c+1<<16>>16;n[t+((c&65535)<<2)>>2]=e;switch(r<<16>>16){case 39:{d(39);break}case 34:{d(34);break}default:{n[61]=(n[61]|0)+-2;break e}}e=(n[61]|0)+2|0;n[61]=e;switch((w(1)|0)<<16>>16){case 44:{n[61]=(n[61]|0)+2;w(1)|0;r=n[51]|0;n[r+4>>2]=e;c=n[61]|0;n[r+16>>2]=c;s[r+24>>0]=1;n[61]=c+-2;break e}case 41:{a[379]=(a[379]|0)+-1<<16>>16;c=n[51]|0;n[c+4>>2]=e;n[c+12>>2]=(n[61]|0)+2;s[c+24>>0]=1;a[378]=(a[378]|0)+-1<<16>>16;break e}default:{n[61]=(n[61]|0)+-2;break e}}}break}case 46:{n[61]=(n[61]|0)+2;if(((w(1)|0)<<16>>16==109?(e=n[61]|0,(m(e+2|0,44,6)|0)==0):0)?(a[n[57]>>1]|0)!=46:0)v(c,c,e+8|0,2);break}case 42:case 39:case 34:{i=16;break}case 123:{e=n[61]|0;if(a[379]|0){n[61]=e+-2;break e}while(1){if(e>>>0>=(n[62]|0)>>>0)break;e=w(1)|0;if(!(N(e)|0)){if(e<<16>>16==125){i=31;break}}else d(e);e=(n[61]|0)+2|0;n[61]=e}if((i|0)==31)n[61]=(n[61]|0)+2;w(1)|0;e=n[61]|0;if(m(e,50,8)|0){L();break e}n[61]=e+8;e=w(1)|0;if(N(e)|0){u(c,e);break e}else{L();break e}}default:if((n[61]|0)!=(t|0))i=16}}while(0);do{if((i|0)==16){if(a[379]|0){n[61]=(n[61]|0)+-2;break}e=n[62]|0;t=n[61]|0;while(1){if(t>>>0>=e>>>0){i=23;break}r=a[t>>1]|0;if(N(r)|0){i=21;break}i=t+2|0;n[61]=i;t=i}if((i|0)==21){u(c,r);break}else if((i|0)==23){L();break}}}while(0);return}function u(e,t){e=e|0;t=t|0;var r=0,s=0;r=(n[61]|0)+2|0;switch(t<<16>>16){case 39:{d(39);s=5;break}case 34:{d(34);s=5;break}default:L()}do{if((s|0)==5){v(e,r,n[61]|0,1);n[61]=(n[61]|0)+2;s=(w(0)|0)<<16>>16==97;t=n[61]|0;if(s?(m(t+2|0,58,10)|0)==0:0){n[61]=t+12;if((w(1)|0)<<16>>16!=123){n[61]=t;break}e=n[61]|0;r=e;e:while(1){n[61]=r+2;r=w(1)|0;switch(r<<16>>16){case 39:{d(39);n[61]=(n[61]|0)+2;r=w(1)|0;break}case 34:{d(34);n[61]=(n[61]|0)+2;r=w(1)|0;break}default:r=E(r)|0}if(r<<16>>16!=58){s=16;break}n[61]=(n[61]|0)+2;switch((w(1)|0)<<16>>16){case 39:{d(39);break}case 34:{d(34);break}default:{s=20;break e}}n[61]=(n[61]|0)+2;switch((w(1)|0)<<16>>16){case 125:{s=25;break e}case 44:break;default:{s=24;break e}}n[61]=(n[61]|0)+2;if((w(1)|0)<<16>>16==125){s=25;break}r=n[61]|0}if((s|0)==16){n[61]=t;break}else if((s|0)==20){n[61]=t;break}else if((s|0)==24){n[61]=t;break}else if((s|0)==25){s=n[51]|0;n[s+16>>2]=e;n[s+12>>2]=(n[61]|0)+2;break}}n[61]=t+-2}}while(0);return}function o(e){e=e|0;e:do{switch(a[e>>1]|0){case 100:switch(a[e+-2>>1]|0){case 105:{e=S(e+-4|0,68,2)|0;break e}case 108:{e=S(e+-4|0,72,3)|0;break e}default:{e=0;break e}}case 101:{switch(a[e+-2>>1]|0){case 115:break;case 116:{e=S(e+-4|0,78,4)|0;break e}default:{e=0;break e}}switch(a[e+-4>>1]|0){case 108:{e=O(e+-6|0,101)|0;break e}case 97:{e=O(e+-6|0,99)|0;break e}default:{e=0;break e}}}case 102:{if((a[e+-2>>1]|0)==111?(a[e+-4>>1]|0)==101:0)switch(a[e+-6>>1]|0){case 99:{e=S(e+-8|0,86,6)|0;break e}case 112:{e=S(e+-8|0,98,2)|0;break e}default:{e=0;break e}}else e=0;break}case 110:{e=e+-2|0;if(O(e,105)|0)e=1;else e=S(e,102,5)|0;break}case 111:{e=O(e+-2|0,100)|0;break}case 114:{e=S(e+-2|0,112,7)|0;break}case 116:{e=S(e+-2|0,126,4)|0;break}case 119:switch(a[e+-2>>1]|0){case 101:{e=O(e+-4|0,110)|0;break e}case 111:{e=S(e+-4|0,134,3)|0;break e}default:{e=0;break e}}default:e=0}}while(0);return e|0}function h(){var e=0,t=0,r=0;t=n[62]|0;r=n[61]|0;e:while(1){e=r+2|0;if(r>>>0>=t>>>0){t=8;break}switch(a[e>>1]|0){case 96:{t=9;break e}case 36:{if((a[r+4>>1]|0)==123){t=6;break e}break}case 92:{e=r+4|0;break}default:{}}r=e}if((t|0)==6){n[61]=r+4;e=a[380]|0;t=n[58]|0;r=a[377]|0;a[377]=r+1<<16>>16;a[t+((r&65535)<<1)>>1]=e;r=(a[379]|0)+1<<16>>16;a[379]=r;a[380]=r}else if((t|0)==8){n[61]=e;L()}else if((t|0)==9)n[61]=e;return}function w(e){e=e|0;var t=0,r=0,s=0;r=n[61]|0;e:do{t=a[r>>1]|0;t:do{if(t<<16>>16!=47)if(e)if(M(t)|0)break;else break e;else if(z(t)|0)break;else break e;else switch(a[r+2>>1]|0){case 47:{j();break t}case 42:{y(e);break t}default:{t=47;break e}}}while(0);s=n[61]|0;r=s+2|0;n[61]=r}while(s>>>0<(n[62]|0)>>>0);return t|0}function d(e){e=e|0;var t=0,r=0,s=0,i=0;i=n[62]|0;t=n[61]|0;while(1){s=t+2|0;if(t>>>0>=i>>>0){t=9;break}r=a[s>>1]|0;if(r<<16>>16==e<<16>>16){t=10;break}if(r<<16>>16==92){r=t+4|0;if((a[r>>1]|0)==13){t=t+6|0;t=(a[t>>1]|0)==10?t:r}else t=r}else if(T(r)|0){t=9;break}else t=s}if((t|0)==9){n[61]=s;L()}else if((t|0)==10)n[61]=s;return}function v(e,t,r,a){e=e|0;t=t|0;r=r|0;a=a|0;var i=0,c=0;i=n[55]|0;n[55]=i+32;c=n[51]|0;n[((c|0)==0?188:c+28|0)>>2]=i;n[52]=c;n[51]=i;n[i+8>>2]=e;if(2==(a|0))e=r;else e=1==(a|0)?r+2|0:0;n[i+12>>2]=e;n[i>>2]=t;n[i+4>>2]=r;n[i+16>>2]=0;n[i+20>>2]=a;s[i+24>>0]=1==(a|0)&1;n[i+28>>2]=0;return}function A(){var e=0,t=0,r=0;r=n[62]|0;t=n[61]|0;e:while(1){e=t+2|0;if(t>>>0>=r>>>0){t=6;break}switch(a[e>>1]|0){case 13:case 10:{t=6;break e}case 93:{t=7;break e}case 92:{e=t+4|0;break}default:{}}t=e}if((t|0)==6){n[61]=e;L();e=0}else if((t|0)==7){n[61]=e;e=93}return e|0}function C(e,t){e=e|0;t=t|0;var r=0,s=0;r=n[61]|0;s=a[r>>1]|0;if(s<<16>>16==97){n[61]=r+4;r=w(1)|0;e=n[61]|0;if(N(r)|0){d(r);t=(n[61]|0)+2|0;n[61]=t}else{E(r)|0;t=n[61]|0}s=w(1)|0;r=n[61]|0}if((r|0)!=(e|0))B(e,t);return s|0}function g(){var e=0,t=0,r=0;e:while(1){e=n[61]|0;t=e+2|0;n[61]=t;if(e>>>0>=(n[62]|0)>>>0){r=7;break}switch(a[t>>1]|0){case 13:case 10:{r=7;break e}case 47:break e;case 91:{A()|0;break}case 92:{n[61]=e+4;break}default:{}}}if((r|0)==7)L();return}function p(e){e=e|0;switch(a[e>>1]|0){case 62:{e=(a[e+-2>>1]|0)==61;break}case 41:case 59:{e=1;break}case 104:{e=S(e+-2|0,160,4)|0;break}case 121:{e=S(e+-2|0,168,6)|0;break}case 101:{e=S(e+-2|0,180,3)|0;break}default:e=0}return e|0}function y(e){e=e|0;var t=0,r=0,s=0,i=0,c=0;i=(n[61]|0)+2|0;n[61]=i;r=n[62]|0;while(1){t=i+2|0;if(i>>>0>=r>>>0)break;s=a[t>>1]|0;if(!e?T(s)|0:0)break;if(s<<16>>16==42?(a[i+4>>1]|0)==47:0){c=8;break}i=t}if((c|0)==8){n[61]=t;t=i+4|0}n[61]=t;return}function m(e,t,r){e=e|0;t=t|0;r=r|0;var a=0,n=0;e:do{if(!r)e=0;else{while(1){a=s[e>>0]|0;n=s[t>>0]|0;if(a<<24>>24!=n<<24>>24)break;r=r+-1|0;if(!r){e=0;break e}else{e=e+1|0;t=t+1|0}}e=(a&255)-(n&255)|0}}while(0);return e|0}function I(e){e=e|0;e:do{switch(e<<16>>16){case 38:case 37:case 33:{e=1;break}default:if((e&-8)<<16>>16==40|(e+-58&65535)<6)e=1;else{switch(e<<16>>16){case 91:case 93:case 94:{e=1;break e}default:{}}e=(e+-123&65535)<4}}}while(0);return e|0}function U(e){e=e|0;e:do{switch(e<<16>>16){case 38:case 37:case 33:break;default:if(!((e+-58&65535)<6|(e+-40&65535)<7&e<<16>>16!=41)){switch(e<<16>>16){case 91:case 94:break e;default:{}}return e<<16>>16!=125&(e+-123&65535)<4|0}}}while(0);return 1}function x(e){e=e|0;var t=0,r=0,s=0,i=0;r=f;f=f+16|0;s=r;n[s>>2]=0;n[54]=e;t=n[3]|0;i=t+(e<<1)|0;e=i+2|0;a[i>>1]=0;n[s>>2]=e;n[55]=e;n[47]=0;n[51]=0;n[49]=0;n[48]=0;n[53]=0;n[50]=0;f=r;return t|0}function S(e,t,r){e=e|0;t=t|0;r=r|0;var s=0,i=0;s=e+(0-r<<1)|0;i=s+2|0;e=n[3]|0;if(i>>>0>=e>>>0?(m(i,t,r<<1)|0)==0:0)if((i|0)==(e|0))e=1;else e=$(a[s>>1]|0)|0;else e=0;return e|0}function O(e,t){e=e|0;t=t|0;var r=0;r=n[3]|0;if(r>>>0<=e>>>0?(a[e>>1]|0)==t<<16>>16:0)if((r|0)==(e|0))r=1;else r=$(a[e+-2>>1]|0)|0;else r=0;return r|0}function $(e){e=e|0;e:do{if((e+-9&65535)<5)e=1;else{switch(e<<16>>16){case 32:case 160:{e=1;break e}default:{}}e=e<<16>>16!=46&(I(e)|0)}}while(0);return e|0}function j(){var e=0,t=0,r=0;e=n[62]|0;r=n[61]|0;e:while(1){t=r+2|0;if(r>>>0>=e>>>0)break;switch(a[t>>1]|0){case 13:case 10:break e;default:r=t}}n[61]=t;return}function B(e,t){e=e|0;t=t|0;var r=0,s=0;r=n[55]|0;n[55]=r+12;s=n[53]|0;n[((s|0)==0?192:s+8|0)>>2]=r;n[53]=r;n[r>>2]=e;n[r+4>>2]=t;n[r+8>>2]=0;return}function E(e){e=e|0;while(1){if(M(e)|0)break;if(I(e)|0)break;e=(n[61]|0)+2|0;n[61]=e;e=a[e>>1]|0;if(!(e<<16>>16)){e=0;break}}return e|0}function P(){var e=0;e=n[(n[49]|0)+20>>2]|0;switch(e|0){case 1:{e=-1;break}case 2:{e=-2;break}default:e=e-(n[3]|0)>>1}return e|0}function q(e){e=e|0;if(!(S(e,140,5)|0)?!(S(e,150,3)|0):0)e=S(e,156,2)|0;else e=1;return e|0}function z(e){e=e|0;switch(e<<16>>16){case 160:case 32:case 12:case 11:case 9:{e=1;break}default:e=0}return e|0}function D(e){e=e|0;if((n[3]|0)==(e|0))e=1;else e=$(a[e+-2>>1]|0)|0;return e|0}function F(){var e=0;e=n[(n[49]|0)+12>>2]|0;if(!e)e=-1;else e=e-(n[3]|0)>>1;return e|0}function G(){var e=0;e=n[(n[49]|0)+16>>2]|0;if(!e)e=-1;else e=e-(n[3]|0)>>1;return e|0}function H(){var e=0;e=n[(n[49]|0)+4>>2]|0;if(!e)e=-1;else e=e-(n[3]|0)>>1;return e|0}function J(){var e=0;e=n[49]|0;e=n[((e|0)==0?188:e+28|0)>>2]|0;n[49]=e;return(e|0)!=0|0}function K(){var e=0;e=n[50]|0;e=n[((e|0)==0?192:e+8|0)>>2]|0;n[50]=e;return(e|0)!=0|0}function L(){s[762]=1;n[56]=(n[61]|0)-(n[3]|0)>>1;n[61]=(n[62]|0)+2;return}function M(e){e=e|0;return(e|128)<<16>>16==160|(e+-9&65535)<5|0}function N(e){e=e|0;return e<<16>>16==39|e<<16>>16==34|0}function Q(){return(n[(n[49]|0)+8>>2]|0)-(n[3]|0)>>1|0}function R(){return(n[(n[50]|0)+4>>2]|0)-(n[3]|0)>>1|0}function T(e){e=e|0;return e<<16>>16==13|e<<16>>16==10|0}function V(){return(n[n[49]>>2]|0)-(n[3]|0)>>1|0}function W(){return(n[n[50]>>2]|0)-(n[3]|0)>>1|0}function X(){return i[(n[49]|0)+24>>0]|0|0}function Y(e){e=e|0;n[3]=e;return}function Z(){return(s[763]|0)!=0|0}function _(){return n[56]|0}function ee(e){e=e|0;f=e+992+15&-16;return 992}return{su:ee,ai:G,e:_,ee:R,es:W,f:Z,id:P,ie:H,ip:X,is:V,p:b,re:K,ri:J,sa:x,se:F,ses:Y,ss:Q}}("undefined"!=typeof self?self:global,{},$e),Le=Se.su(Oe-(2<<17))}const s=Ue.length+1;Se.ses(Le),Se.sa(s-1),Ce(Ue,new Uint16Array($e,Le,s)),Se.p()||(Me=Se.e(),o());const a=[],n=[];for(;Se.ri();){const e=Se.is(),t=Se.ie(),r=Se.ai(),s=Se.id(),n=Se.ss(),i=Se.se();let c;Se.ip()&&(c=b(-1===s?e:e+1,Ue.charCodeAt(-1===s?e-1:e))),a.push({n:c,s:e,e:t,ss:n,se:i,d:s,a:r})}for(;Se.re();){const e=Se.es(),t=Ue.charCodeAt(e);n.push(34===t||39===t?b(e+1,t):Ue.slice(Se.es(),Se.ee()))}return[a,n,!!Se.f()]}function b(e,t){Me=e;let r="",s=Me;for(;;){Me>=Ue.length&&o();const e=Ue.charCodeAt(Me);if(e===t)break;92===e?(r+=Ue.slice(s,Me),r+=k(),s=Me):(8232===e||8233===e||u(e)&&o(),++Me)}return r+=Ue.slice(s,Me++),r}function k(){let e=Ue.charCodeAt(++Me);switch(++Me,e){case 110:return"\n";case 114:return"\r";case 120:return String.fromCharCode(l(2));case 117:return function(){let e;123===Ue.charCodeAt(Me)?(++Me,e=l(Ue.indexOf("}",Me)-Me),++Me,e>1114111&&o()):e=l(4);return e<=65535?String.fromCharCode(e):(e-=65536,String.fromCharCode(55296+(e>>10),56320+(1023&e)))}();case 116:return"\t";case 98:return"\b";case 118:return"\v";case 102:return"\f";case 13:10===Ue.charCodeAt(Me)&&++Me;case 10:return"";case 56:case 57:o();default:if(e>=48&&e<=55){let t=Ue.substr(Me-1,3).match(/^[0-7]+/)[0],r=parseInt(t,8);return r>255&&(t=t.slice(0,-1),r=parseInt(t,8)),Me+=t.length-1,e=Ue.charCodeAt(Me),"0"===t&&56!==e&&57!==e||o(),String.fromCharCode(r)}return u(e)?"":String.fromCharCode(e)}}function l(e){const t=Me;let r=0,s=0;for(let t=0;t<e;++t,++Me){let e,a=Ue.charCodeAt(Me);if(95!==a){if(a>=97)e=a-97+10;else if(a>=65)e=a-65+10;else{if(!(a>=48&&a<=57))break;e=a-48}if(e>=16)break;s=a,r=16*r+e}else 95!==s&&0!==t||o(),s=a}return 95!==s&&Me-t===e||o(),r}function u(e){return 13===e||10===e}function o(){throw Object.assign(Error(`Parse error ${Ie}:${Ue.slice(0,Me).split("\n").length}:${Me-Ue.lastIndexOf("\n",Me-1)}`),{idx:Me})}async function _resolve(e,t){const r=resolveIfNotPlainOrUrl(e,t);return{r:resolveImportMap(Ee,r||e,t)||throwUnresolved(e,t),b:!r&&!isURL(e)}}const Pe=a?async(e,t)=>{let r=a(e,t,defaultResolve);r&&r.then&&(r=await r);return r?{r:r,b:!resolveIfNotPlainOrUrl(e,t)&&!isURL(e)}:_resolve(e,t)}:_resolve;async function importShim(e,...t){let a=t[t.length-1];"string"!==typeof a&&(a=ue);await Ne;s&&await s(e,"string"!==typeof t[1]?t[1]:{},a);if(Te||r||!Re){processImportMaps();r||(Te=false)}await _e;return topLevelLoad((await Pe(e,a)).r,{credentials:"same-origin"})}self.importShim=importShim;function defaultResolve(e,t){return resolveImportMap(Ee,resolveIfNotPlainOrUrl(e,t)||e,t)||throwUnresolved(e,t)}function throwUnresolved(e,t){throw Error(`Unable to resolve specifier '${e}'${fromParent(t)}`)}const resolveSync=(e,t=ue)=>{t=`${t}`;const r=a&&a(e,t,defaultResolve);return r&&!r.then?r:defaultResolve(e,t)};function metaResolve(e,t=this.url){return resolveSync(e,t)}importShim.resolve=resolveSync;importShim.getImportMap=()=>JSON.parse(JSON.stringify(Ee));const xe=importShim._r={};async function loadAll(e,t){if(!e.b&&!t[e.u]){t[e.u]=1;await e.L;await Promise.all(e.d.map((e=>loadAll(e,t))));e.n||(e.n=e.d.some((e=>e.n)))}}let Ee={imports:{},scopes:{}};let je=false;let Re;const Ne=ye.then((()=>{if(!r)if(document.querySelectorAll("script[type=module-shim],script[type=importmap-shim],link[rel=modulepreload-shim]").length)setShimMode();else{let e=false;for(const t of document.querySelectorAll("script[type=module],script[type=importmap]"))if(e){if("importmap"===t.type){je=true;break}}else"module"===t.type&&(e=true)}Re=true!==t.polyfillEnable&&ve&&we&&ge&&(!le||ke)&&(!ce||me)&&!je&&true;if(!r&&Re);else{new MutationObserver((e=>{for(const t of e)if("childList"===t.type)for(const e of t.addedNodes)if("SCRIPT"===e.tagName){e.type===(r?"module-shim":"module")&&processScript(e);e.type===(r?"importmap-shim":"importmap")&&processImportMap(e)}else"LINK"===e.tagName&&e.rel===(r?"modulepreload-shim":"modulepreload")&&processPreload(e)})).observe(document,{childList:true,subtree:true});processImportMaps();processScriptsAndPreloads()}}));let _e=Ne;let Be=true;let Te=true;async function topLevelLoad(e,t,a,n,i){r||(Te=false);await _e;s&&await s(id,"string"!==typeof args[1]?args[1]:{},parentUrl);if(!r&&Re){if(n)return null;await i;return be(a?createBlob(a):e,{errUrl:e||a})}const c=getOrCreateLoad(e,t,null,a);const f={};await loadAll(c,f);Fe=void 0;resolveDeps(c,f);await i;if(a&&!r&&!c.n&&true){const e=await be(createBlob(a),{errUrl:a});ae&&revokeObjectURLs(Object.keys(f));return e}if(Be&&!r&&c.n&&n){se();Be=false}const te=await be(r||c.n||!n?c.b:c.u,{errUrl:c.u});c.s&&(await be(c.s)).u$_(te);ae&&revokeObjectURLs(Object.keys(f));return te}function revokeObjectURLs(e){let t=0;const r=e.length;const s=self.requestIdleCallback?self.requestIdleCallback:self.requestAnimationFrame;s(cleanup);function cleanup(){const a=100*t;if(!(a>r)){for(const t of e.slice(a,a+100)){const e=xe[t];e&&URL.revokeObjectURL(e.b)}t++;s(cleanup)}}}function urlJsString(e){return`'${e.replace(/'/g,"\\'")}'`}let Fe;function resolveDeps(e,t){if(e.b||!t[e.u])return;t[e.u]=0;for(const c of e.d)resolveDeps(c,t);const[r]=e.a;const s=e.S;let a=fe&&Fe?`import '${Fe}';`:"";if(r.length){let f=0,te=0,re=[];function pushStringTo(t){while(re[re.length-1]<t){const t=re.pop();a+=`${s.slice(f,t)}, ${urlJsString(e.r)}`;f=t}a+=s.slice(f,t);f=t}for(const{s:se,ss:ae,se:ne,d:ie}of r)if(-1===ie){let oe=e.d[te++],ce=oe.b,le=!ce;le&&((ce=oe.s)||(ce=oe.s=createBlob(`export function u$_(m){${oe.a[1].map((e=>"default"===e?"d$_=m.default":`${e}=m.${e}`)).join(",")}}${oe.a[1].map((e=>"default"===e?"let d$_;export{d$_ as default}":`export let ${e}`)).join(";")}\n//# sourceURL=${oe.r}?cycle`)));pushStringTo(se-1);a+=`/*${s.slice(se-1,ne)}*/${urlJsString(ce)}`;if(!le&&oe.s){a+=`;import*as m$_${te} from'${oe.b}';import{u$_ as u$_${te}}from'${oe.s}';u$_${te}(m$_${te})`;oe.s=void 0}f=ne}else if(-2===ie){e.m={url:e.r,resolve:metaResolve};i(e.m,e.u);pushStringTo(se);a+=`importShim._r[${urlJsString(e.u)}].m`;f=ne}else{pushStringTo(ae+6);a+="Shim(";re.push(ne-1);f=se}pushStringTo(s.length)}else a+=s;let n=false;a=a.replace(He,((t,r,s)=>(n=!r,t.replace(s,(()=>new URL(s,e.r))))));n||(a+="\n//# sourceURL="+e.r);e.b=Fe=createBlob(a);e.S=void 0}const He=/\n\/\/# source(Mapping)?URL=([^\n]+)\s*((;|\/\/[^#][^\n]*)\s*)*$/;const qe=/^(text|application)\/(x-)?javascript(;|$)/;const De=/^(text|application)\/json(;|$)/;const Je=/^(text|application)\/css(;|$)/;const Ke=/url\(\s*(?:(["'])((?:\\.|[^\n\\"'])+)\1|((?:\\.|[^\s,"'()\\])+))\s*\)/g;let ze=[];let Ge=0;function pushFetchPool(){if(++Ge>100)return new Promise((e=>ze.push(e)))}function popFetchPool(){Ge--;ze.length&&ze.shift()()}async function doFetch(e,t,r){if(ie&&!t.integrity)throw Error(`No integrity for ${e}${fromParent(r)}.`);const s=pushFetchPool();s&&await s;try{var a=await n(e,t)}catch(t){t.message=`Unable to fetch ${e}${fromParent(r)} - see network log for details.\n`+t.message;throw t}finally{popFetchPool()}if(!a.ok)throw Error(`${a.status} ${a.statusText} ${a.url}${fromParent(r)}`);return a}async function fetchModule(e,t,r){const s=await doFetch(e,t,r);const a=s.headers.get("content-type");if(qe.test(a))return{r:s.url,s:await s.text(),t:"js"};if(De.test(a))return{r:s.url,s:`export default ${await s.text()}`,t:"json"};if(Je.test(a))return{r:s.url,s:`var s=new CSSStyleSheet();s.replaceSync(${JSON.stringify((await s.text()).replace(Ke,((t,r="",s,a)=>`url(${r}${resolveUrl(s||a,e)}${r})`)))});export default s;`,t:"css"};throw Error(`Unsupported Content-Type "${a}" loading ${e}${fromParent(r)}. Modules must be served with a valid MIME type like application/javascript.`)}function getOrCreateLoad(e,t,s,a){let n=xe[e];if(n&&!a)return n;n={u:e,r:a?e:void 0,f:void 0,S:void 0,L:void 0,a:void 0,d:void 0,b:void 0,s:void 0,n:false,t:null,m:null};if(xe[e]){let e=0;while(xe[n.u+ ++e]);n.u+=e}xe[n.u]=n;n.f=(async()=>{if(!a){let i;({r:n.r,s:a,t:i}=await(Xe[e]||fetchModule(e,t,s)));if(i&&!r){if("css"===i&&!ce||"json"===i&&!le)throw Error(`${i}-modules require <script type="esms-options">{ "polyfillEnable": ["${i}-modules"] }<\/script>`);("css"===i&&!me||"json"===i&&!ke)&&(n.n=true)}}try{n.a=parse(a,n.u)}catch(e){throwError(e);n.a=[[],[],false]}n.S=a;return n})();n.L=n.f.then((async()=>{let e=t;n.d=(await Promise.all(n.a[0].map((async({n:t,d:r})=>{(r>=0&&!ve||2===r&&!we)&&(n.n=true);if(-1!==r||!t)return;const{r:s,b:a}=await Pe(t,n.r||n.u);!a||ge&&!je||(n.n=true);if(c&&c.test(s))return{b:s};e.integrity&&(e=Object.assign({},e,{integrity:void 0}));return getOrCreateLoad(s,e,n.r).f})))).filter((e=>e))}));return n}function processScriptsAndPreloads(){for(const e of document.querySelectorAll(r?"script[type=module-shim]":"script[type=module]"))processScript(e);for(const e of document.querySelectorAll(r?"link[rel=modulepreload-shim]":"link[rel=modulepreload]"))processPreload(e)}function processImportMaps(){for(const e of document.querySelectorAll(r?'script[type="importmap-shim"]':'script[type="importmap"]'))processImportMap(e)}function getFetchOpts(e){const t={};e.integrity&&(t.integrity=e.integrity);e.referrerpolicy&&(t.referrerPolicy=e.referrerpolicy);"use-credentials"===e.crossorigin?t.credentials="include":"anonymous"===e.crossorigin?t.credentials="omit":t.credentials="same-origin";return t}let Qe=Promise.resolve();let Ve=1;function domContentLoadedCheck(){0!==--Ve||ne||document.dispatchEvent(new Event("DOMContentLoaded"))}document.addEventListener("DOMContentLoaded",(async()=>{await Ne;domContentLoadedCheck();if(r||!Re){processImportMaps();processScriptsAndPreloads()}}));let We=1;"complete"===document.readyState?readyStateCompleteCheck():document.addEventListener("readystatechange",(async()=>{processImportMaps();await Ne;readyStateCompleteCheck()}));function readyStateCompleteCheck(){0!==--We||ne||document.dispatchEvent(new Event("readystatechange"))}function processImportMap(e){if(!e.ep&&(e.src||e.innerHTML)){e.ep=true;if(e.src){if(!r)return;je=true}if(Te){_e=_e.then((async()=>{Ee=resolveAndComposeImportMap(e.src?await(await doFetch(e.src,getFetchOpts(e))).json():JSON.parse(e.innerHTML),e.src||ue,Ee)})).catch(throwError);r||(Te=false)}}}function processScript(e){if(e.ep)return;if(null!==e.getAttribute("noshim"))return;if(!e.src&&!e.innerHTML)return;e.ep=true;const t=We>0;const s=Ve>0;t&&We++;s&&Ve++;const a=null===e.getAttribute("async")&&t;const n=topLevelLoad(e.src||ue,getFetchOpts(e),!e.src&&e.innerHTML,!r,a&&Qe).catch(throwError);a&&(Qe=n.then(readyStateCompleteCheck));s&&n.then(domContentLoadedCheck)}const Xe={};function processPreload(e){if(!e.ep){e.ep=true;Xe[e.href]||(Xe[e.href]=fetchModule(e.href,getFetchOpts(e)))}}})();
@@ -3,10 +3,21 @@ module GoodJob
3
3
  class AssetsController < ActionController::Base # rubocop:disable Rails/ApplicationController
4
4
  skip_before_action :verify_authenticity_token, raise: false
5
5
 
6
+ def self.js_modules
7
+ @_js_modules ||= GoodJob::Engine.root.join("app", "assets", "modules").children.select(&:file?).each_with_object({}) do |file, modules|
8
+ key = File.basename(file.basename.to_s, ".js").to_sym
9
+ modules[key] = file
10
+ end
11
+ end
12
+
6
13
  before_action do
7
14
  expires_in 1.year, public: true
8
15
  end
9
16
 
17
+ def es_module_shims_js
18
+ render file: GoodJob::Engine.root.join("app", "assets", "vendor", "es_module_shims.js")
19
+ end
20
+
10
21
  def bootstrap_css
11
22
  render file: GoodJob::Engine.root.join("app", "assets", "vendor", "bootstrap", "bootstrap.min.css")
12
23
  end
@@ -30,5 +41,11 @@ module GoodJob
30
41
  def style_css
31
42
  render file: GoodJob::Engine.root.join("app", "assets", "style.css")
32
43
  end
44
+
45
+ def modules_js
46
+ module_name = params[:module].to_sym
47
+ module_file = self.class.js_modules.fetch(module_name) { raise ActionController::RoutingError, 'Not Found' }
48
+ render file: module_file
49
+ end
33
50
  end
34
51
  end
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  class ExecutionsController < GoodJob::ApplicationController
4
- def index
5
- @filter = ExecutionsFilter.new(params)
6
- end
7
-
8
4
  def destroy
9
5
  deleted_count = GoodJob::Execution.where(id: params[:id]).delete_all
10
6
  message = deleted_count.positive? ? { notice: "Job execution deleted" } : { alert: "Job execution not deleted" }
11
- redirect_back fallback_location: root_path, **message
7
+ redirect_back fallback_location: jobs_path, **message
12
8
  end
13
9
  end
14
10
  end
@@ -12,7 +12,7 @@ module GoodJob
12
12
  def show
13
13
  @executions = GoodJob::Execution.active_job_id(params[:id])
14
14
  .order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
15
- redirect_to root_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
15
+ redirect_to jobs_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
16
16
  end
17
17
 
18
18
  def discard
@@ -1,4 +1,6 @@
1
- <h1 class="mb-3">Cron Entry Key: <code><%= @cron_entry.id %></code></h1>
1
+ <% title = capture do %>
2
+ Cron Entry Key: <code><%= @cron_entry.id %></code>
3
+ <% end %>
2
4
 
3
- <%= render 'good_job/shared/filter', filter: @jobs_filter %>
5
+ <%= render 'good_job/shared/filter', title: title, filter: @jobs_filter %>
4
6
  <%= render 'good_job/jobs/table', jobs: @jobs_filter.records %>
@@ -1,6 +1,6 @@
1
- <div class="card my-3" data-gj-poll-replace id="executions-table">
1
+ <div class="my-3" data-gj-poll-replace id="executions-table">
2
2
  <div class="table-responsive">
3
- <table class="table card-table table-bordered table-hover table-sm mb-0" id="executions_index_table">
3
+ <table class="table table-hover table-sm mb-0" id="executions_index_table">
4
4
  <thead>
5
5
  <tr>
6
6
  <th>ActiveJob ID</th>
@@ -1,6 +1,6 @@
1
- <div class="card my-3" data-gj-poll-replace id="jobs-table">
1
+ <div class="my-3" data-gj-poll-replace id="jobs-table">
2
2
  <div class="table-responsive">
3
- <table class="table card-table table-bordered table-hover table-sm mb-0">
3
+ <table class="table table-hover table-sm mb-0">
4
4
  <thead>
5
5
  <tr>
6
6
  <th>ActiveJob ID</th>
@@ -1,13 +1,5 @@
1
- <div class="my-3 flex">
2
- <h2>All Jobs</h2>
3
- </div>
4
-
5
- <div class="card my-3 p-6" data-gj-poll-replace id="jobs-chart">
6
- <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
7
- </div>
8
-
9
- <%= render 'good_job/shared/filter', filter: @filter %>
10
-
1
+ <%= render 'good_job/shared/filter', title: "Jobs", filter: @filter %>
2
+ <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
11
3
  <%= render 'good_job/jobs/table', jobs: @filter.records %>
12
4
 
13
5
  <% if @filter.records.present? %>
@@ -1,13 +1,20 @@
1
- <% if notice %>
2
- <div class="alert alert-success alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
3
- <%= render "good_job/shared/icons/check", class: "flex-shrink-0 me-2" %>
4
- <div><%= notice %></div>
5
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
6
- </div>
7
- <% elsif alert %>
8
- <div class="alert alert-warning alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
9
- <%= render "good_job/shared/icons/exclamation", class: "flex-shrink-0 me-2" %>
10
- <div><%= alert %></div>
11
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
12
- </div>
13
- <% end %>
1
+ <div class="toast-container position-fixed p-3 start-50 translate-middle-x">
2
+ <% if notice %>
3
+ <div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
4
+ <div class="toast-body d-flex align-items-center gap-2">
5
+ <%= render "good_job/shared/icons/check", class: "flex-shrink-0 text-success" %>
6
+ <div class="flex-fill"><%= notice %></div>
7
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
8
+ </div>
9
+ </div>
10
+ <% end %>
11
+ <% if alert %>
12
+ <div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
13
+ <div class="toast-body d-flex align-items-center gap-2">
14
+ <%= render "good_job/shared/icons/exclamation", class: "flex-shrink-0 text-danger" %>
15
+ <div class="flex-fill"><%= alert %></div>
16
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
17
+ </div>
18
+ </div>
19
+ <% end %>
20
+ </div>
@@ -1,3 +1,5 @@
1
- <div class="chart-wrapper">
2
- <canvas class="chart" data-json="<%= chart_data.to_json %>"></canvas>
1
+ <div class="py-4" data-gj-poll-replace id="chart">
2
+ <div class="chart-wrapper container-fluid">
3
+ <canvas class="chart" data-json="<%= chart_data.to_json %>"></canvas>
4
+ </div>
3
5
  </div>
@@ -1,59 +1,67 @@
1
- <%= form_with(url: "", method: :get, local: true, id: "filter_form") do |form| %>
2
- <%= hidden_field_tag :poll, value: params[:poll] %>
3
- <div class="d-flex flex-row w-100">
4
- <div class="me-2">
5
- <label for="job_class_filter">Job class</label>
6
- <select name="job_class" id="job_class_filter" class="form-select">
7
- <option value="" <%= "selected='selected'" if params[:job_class].blank? %>>All jobs</option>
8
-
9
- <% filter.job_classes.each do |name, count| %>
10
- <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:job_class] == name %>><%= name %> (<%= count %>)</option>
11
- <% end %>
12
- </select>
13
- </div>
1
+ <div data-gj-poll-replace id="filter">
2
+ <div class="bg-light break-out">
3
+ <h2 class="container-fluid pt-3 pb-2"><%= title %></h2>
14
4
 
15
- <div class="me-2">
16
- <label for="job_state_filter">State</label>
17
- <select name="state" id="job_state_filter" class="form-select">
18
- <option value="" <%= "selected='selected'" if params[:state].blank? %>>All states</option>
5
+ <ul class="nav nav-tabs bg-light px-3 mb-3">
6
+ <li class="nav-item">
7
+ <%= link_to "All", url_for(state: nil), class: "nav-link #{"active" unless params[:state]}" %>
8
+ </li>
19
9
 
20
- <% filter.states.each do |name, count| %>
21
- <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:state] == name %>><%= name %> (<%= count %>)</option>
22
- <% end %>
23
- </select>
24
- </div>
10
+ <% filter.states.each do |name, count| %>
11
+ <li class="nav-item">
12
+ <%= link_to url_for({state: name}), class: "nav-link #{"active" if params[:state] == name}" do %>
13
+ <%= name.titleize %>
14
+ <span class="badge bg-primary rounded-pill <%= "bg-secondary" if count == 0 %>"><%= count %></span>
15
+ <% end %>
16
+ </li>
17
+ <% end %>
18
+ </ul>
19
+ </div>
25
20
 
26
- <div class="me-2">
27
- <label for="job_queue_filter">Queue</label>
28
- <select name="queue_name" id="job_queue_filter" class="form-select">
29
- <option value="" <%= "selected='selected'" if params[:queue_name].blank? %>>All queues</option>
21
+ <%= form_with(url: "", method: :get, local: true, id: "filter_form", class: "container-fluid") do |form| %>
22
+ <%= hidden_field_tag :poll, params[:poll] %>
23
+ <%= hidden_field_tag :state, params[:state] %>
24
+ <div class="d-flex flex-row w-100">
25
+ <div class="me-2">
26
+ <select name="job_class" id="job_class_filter" class="form-select form-select-sm">
27
+ <option value="" <%= "selected='selected'" if params[:job_class].blank? %>>All jobs</option>
30
28
 
31
- <% filter.queues.each do |name, count| %>
32
- <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:queue_name] == name %>><%= name %> (<%= count %>)</option>
33
- <% end %>
34
- </select>
35
- </div>
29
+ <% filter.job_classes.each do |name, count| %>
30
+ <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:job_class] == name %>><%= name %> (<%= count %>)</option>
31
+ <% end %>
32
+ </select>
33
+ </div>
36
34
 
37
- <div class="me-2 flex-fill d-flex flex-col align-items-end">
38
- <label class="visually-hidden" for="query" aria-label="Search by class, job id, job params, and error text.">Search by class, job id, job params, and error text.</label>
39
- <%= search_field_tag "query", params[:query], class: "form-control", placeholder: "Search by class, job id, job params, and error text." %>
40
- </div>
35
+ <div class="me-2">
36
+ <select name="queue_name" id="job_queue_filter" class="form-select form-select-sm">
37
+ <option value="" <%= "selected='selected'" if params[:queue_name].blank? %>>All queues</option>
41
38
 
42
- <div class="d-flex flex-col align-items-end">
43
- <div>
44
- <%= form.submit "Search", name: nil, class: "btn btn-primary" %>
45
- <%= link_to "Clear all", filter.to_params(job_class: nil, state: nil, queue_name: nil, query: nil), class: "btn btn-secondary" %>
39
+ <% filter.queues.each do |name, count| %>
40
+ <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:queue_name] == name %>><%= name %> (<%= count %>)</option>
41
+ <% end %>
42
+ </select>
43
+ </div>
44
+
45
+ <div class="me-2 flex-fill">
46
+ <%= search_field_tag "query", params[:query], class: "form-control form-control-sm", placeholder: "Search by class, job id, job params, and error text." %>
47
+ </div>
48
+
49
+ <div class="d-flex flex-col align-items-end">
50
+ <div>
51
+ <%= form.submit "Search", name: nil, class: "btn btn-primary btn-sm" %>
52
+ <%= link_to "Clear all", filter.to_params(job_class: nil, state: nil, queue_name: nil, query: nil), class: "btn btn-secondary btn-sm" %>
53
+ </div>
46
54
  </div>
47
55
  </div>
48
- </div>
49
- <% end %>
50
-
51
- <%= javascript_tag nonce: true do %>
52
- document.addEventListener("DOMContentLoaded", () => {
53
- document.querySelectorAll("#job_class_filter, #job_state_filter, #job_queue_filter").forEach((filter) => {
54
- filter.addEventListener("change", () => {
55
- document.querySelector("#filter_form").submit();
56
- });
56
+ <% end %>
57
+
58
+ <%= javascript_tag nonce: true do %>
59
+ document.addEventListener("DOMContentLoaded", () => {
60
+ document.querySelectorAll("#job_class_filter, #job_queue_filter").forEach((filter) => {
61
+ filter.addEventListener("change", () => {
62
+ document.querySelector("#filter_form").submit();
63
+ });
64
+ })
57
65
  })
58
- })
59
- <% end %>
66
+ <% end %>
67
+ </div>
@@ -1,10 +1,8 @@
1
- <footer class="footer mt-auto py-3 bg-light fixed-bottom" id="footer" data-gj-poll-replace>
1
+ <footer class="footer mt-auto py-3 bg-light border-top text-muted small" id="footer" data-gj-poll-replace>
2
2
  <div class="container-fluid">
3
3
  <div class="row">
4
4
  <div class="col-6">
5
- <span class="text-muted">
6
- <%= t(".last_update_html", time: Time.current.utc.iso8601) %>
7
- </span>
5
+ <%= t(".last_update_html", time: Time.current.utc.iso8601) %>
8
6
  </div>
9
7
 
10
8
  <div class="col-6 text-end">
@@ -1,4 +1,4 @@
1
- <nav class="navbar navbar-expand-lg navbar-light bg-light">
1
+ <nav class="navbar navbar-expand-lg navbar-light border-bottom bg-white sticky-top shadow-sm">
2
2
  <div class="container-fluid">
3
3
  <%= link_to t(".name"), root_path, class: "navbar-brand mb-0 h1" %>
4
4
  <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
@@ -8,21 +8,18 @@
8
8
  <div class="collapse navbar-collapse" id="navbarSupportedContent">
9
9
  <ul class="navbar-nav me-auto">
10
10
  <li class="nav-item">
11
- <%= link_to t(".executions"), root_path, class: ["nav-link", ("active" if current_page?(root_path))] %>
11
+ <%= link_to t(".jobs"), jobs_path, class: ["nav-link", ("active" if controller_name == 'jobs')] %>
12
12
  </li>
13
13
  <li class="nav-item">
14
- <%= link_to t(".jobs"), jobs_path, class: ["nav-link", ("active" if current_page?(jobs_path))] %>
14
+ <%= link_to t(".cron_schedules"), cron_entries_path, class: ["nav-link", ("active" if controller_name == 'cron_entries')] %>
15
15
  </li>
16
16
  <li class="nav-item">
17
- <%= link_to t(".cron_schedules"), cron_entries_path, class: ["nav-link", ("active" if current_page?(cron_entries_path))] %>
18
- </li>
19
- <li class="nav-item">
20
- <%= link_to t(".processes"), processes_path, class: ["nav-link", ("active" if current_page?(processes_path))] %>
17
+ <%= link_to t(".processes"), processes_path, class: ["nav-link", ("active" if controller_name == 'processes')] %>
21
18
  </li>
22
19
  </ul>
23
20
  <div class="nav-item pe-2">
24
21
  <div class="form-check">
25
- <input type="checkbox" id="toggle-poll" name="toggle-poll" data-gj-action='change#togglePoll' <%= 'checked' if params[:poll].present? %>>
22
+ <input type="checkbox" id="toggle-poll" name="toggle-poll" <%= 'checked' if params[:poll].present? %>>
26
23
  <label for="toggle-poll"><%= t(".live_poll") %></label>
27
24
  </div>
28
25
  </div>
@@ -7,25 +7,30 @@
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
 
10
- <%# Assets must use *_url route helpers to avoid being overriden by config.asset_host %>
11
- <%= stylesheet_link_tag bootstrap_url(format: :css, v: GoodJob::VERSION), skip_pipeline: true %>
12
- <%= stylesheet_link_tag style_url(format: :css, v: GoodJob::VERSION) %>
10
+ <%# Do not use asset tag helpers to avoid paths being overriden by config.asset_host %>
11
+ <%= tag.link rel: "stylesheet", media: "screen", href: bootstrap_path(format: :css, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
12
+ <%= tag.link rel: "stylesheet", media: "screen", href: style_path(format: :css, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
13
13
 
14
- <%= javascript_include_tag bootstrap_url(format: :js, v: GoodJob::VERSION), nonce: true %>
15
- <%= javascript_include_tag chartjs_url(format: :js, v: GoodJob::VERSION), nonce: true %>
16
- <%= javascript_include_tag scripts_url(format: :js, v: GoodJob::VERSION), nonce: true %>
14
+ <%= tag.script "", src: bootstrap_path(format: :js, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
15
+ <%= tag.script "", src: chartjs_path(format: :js, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
16
+ <%= tag.script "", src: rails_ujs_path(format: :js, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
17
17
 
18
- <%= javascript_include_tag rails_ujs_url(format: :js, v: GoodJob::VERSION), nonce: true %>
18
+ <%= tag.script "", src: es_module_shims_path(format: :js, v: GoodJob::VERSION, locale: nil), async: true, nonce: content_security_policy_nonce %>
19
+ <% importmaps = { imports: GoodJob::AssetsController.js_modules.keys.each_with_object({}) { |module_name, imports| imports[module_name] = modules_path(module_name, format: :js, locale: nil, v: GoodJob::VERSION) } } %>
20
+ <%= tag.script importmaps.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce %>
21
+ <%= tag.script "", src: scripts_path(format: :js, v: GoodJob::VERSION, locale: nil), type: "module", nonce: content_security_policy_nonce %>
19
22
  </head>
20
23
  <body>
21
- <%= render "good_job/shared/navbar" %>
24
+ <div class="d-flex flex-column min-vh-100">
25
+ <%= render "good_job/shared/navbar" %>
22
26
 
23
- <div class="container-fluid">
24
- <%= render "good_job/shared/alert" %>
27
+ <div class="container-fluid flex-grow-1 relative">
28
+ <%= render "good_job/shared/alert" %>
25
29
 
26
- <%= yield %>
27
- </div>
30
+ <%= yield %>
31
+ </div>
28
32
 
29
33
  <%= render "good_job/shared/footer" %>
34
+ </div>
30
35
  </body>
31
36
  </html>
@@ -45,9 +45,8 @@ en:
45
45
  last_update_html: Last updated <time id="page-updated-at" datetime="%{time}">%{time}</time>
46
46
  wording: Remember, you're doing a Good Job too!
47
47
  navbar:
48
- cron_schedules: Cron Schedules
49
- executions: All Executions
50
- jobs: All Jobs
48
+ cron_schedules: Cron
49
+ jobs: Jobs
51
50
  live_poll: Live Poll
52
51
  name: "GoodJob 👍"
53
52
  processes: Processes
@@ -45,8 +45,7 @@ es:
45
45
  last_update_html: Última actualización <time id="page-updated-at" datetime="%{time}">%{time}</time>
46
46
  wording: "¡Recuerda, también tú estás haciendo un buen trabajo!"
47
47
  navbar:
48
- cron_schedules: Tareas Programadas
49
- executions: Ejecuciones
48
+ cron_schedules: Cron
50
49
  jobs: Tareas
51
50
  live_poll: En vivo
52
51
  name: "GoodJob 👍"
@@ -0,0 +1,52 @@
1
+ ---
2
+ nl:
3
+ datetime:
4
+ distance_in_words:
5
+ about_x_hours:
6
+ one: ongeveer 1 uur
7
+ other: ongeveer %{count} uren
8
+ about_x_months:
9
+ one: ongeveer 1 maand
10
+ other: ongeveer %{count} maanden
11
+ about_x_years:
12
+ one: ongeveer 1 jaar
13
+ other: ongeveer %{count} jaren
14
+ almost_x_years:
15
+ one: bijna 1 jaar
16
+ other: bijna %{count} jaren
17
+ half_a_minute: halve minuut
18
+ less_than_x_minutes:
19
+ one: minder dan één minuut
20
+ other: minder dan %{count} minuten
21
+ less_than_x_seconds:
22
+ one: minder dan 1 seconde
23
+ other: minder dan %{count} seconden
24
+ over_x_years:
25
+ one: meer dan 1 jaar
26
+ other: meer dan %{count} jaren
27
+ x_days:
28
+ one: 1 dag
29
+ other: "%{count} dagen"
30
+ x_minutes:
31
+ one: 1 minuut
32
+ other: "%{count} minuten"
33
+ x_months:
34
+ one: 1 maand
35
+ other: "%{count} maanden"
36
+ x_seconds:
37
+ one: 1 seconde
38
+ other: "%{count} seconden"
39
+ x_years:
40
+ one: 1 jaar
41
+ other: "%{count} jaren"
42
+ good_job:
43
+ shared:
44
+ footer:
45
+ last_update_html: Laatst bijgewerkt <time id="page-updated-at" datetime="%{time}">%{time}</time>
46
+ wording: 'Onthoud: jij levert ook goed werk!'
47
+ navbar:
48
+ cron_schedules: Cron
49
+ jobs: Taken
50
+ live_poll: Live Poll
51
+ name: "GoodJob 👍"
52
+ processes: Processen
@@ -0,0 +1,76 @@
1
+ ---
2
+ ru:
3
+ datetime:
4
+ distance_in_words:
5
+ about_x_hours:
6
+ few: около %{count} часов
7
+ many: около %{count} часов
8
+ one: около 1 часа
9
+ other: около %{count} часа
10
+ about_x_months:
11
+ few: около %{count} месяцев
12
+ many: около %{count} месяцев
13
+ one: около 1 месяца
14
+ other: около %{count} месяца
15
+ about_x_years:
16
+ few: около %{count} лет
17
+ many: около %{count} лет
18
+ one: около 1 года
19
+ other: около %{count} лет
20
+ almost_x_years:
21
+ few: почти %{count} года
22
+ many: почти %{count} лет
23
+ one: почти 1 год
24
+ other: почти %{count} лет
25
+ half_a_minute: полминуты
26
+ less_than_x_minutes:
27
+ few: меньше %{count} минут
28
+ many: меньше %{count} минут
29
+ one: меньше 1 минуты
30
+ other: меньше %{count} минуты
31
+ less_than_x_seconds:
32
+ few: меньше %{count} секунд
33
+ many: меньше %{count} секунд
34
+ one: меньше 1 секунды
35
+ other: меньше %{count} секунды
36
+ over_x_years:
37
+ few: больше %{count} лет
38
+ many: больше %{count} лет
39
+ one: больше 1 года
40
+ other: больше %{count} лет
41
+ x_days:
42
+ few: "%{count} дня"
43
+ many: "%{count} дней"
44
+ one: 1 день
45
+ other: "%{count} дня"
46
+ x_minutes:
47
+ few: "%{count} минуты"
48
+ many: "%{count} минут"
49
+ one: 1 минуту
50
+ other: "%{count} минуты"
51
+ x_months:
52
+ few: "%{count} месяца"
53
+ many: "%{count} месяцев"
54
+ one: 1 месяц
55
+ other: "%{count} месяца"
56
+ x_seconds:
57
+ few: "%{count} секунды"
58
+ many: "%{count} секунд"
59
+ one: 1 секунду
60
+ other: "%{count} секунды"
61
+ x_years:
62
+ few: "%{count} года"
63
+ many: "%{count} лет"
64
+ one: 1 год
65
+ other: "%{count} года"
66
+ good_job:
67
+ shared:
68
+ footer:
69
+ last_update_html: Последнее обновление <time id="page-updated-at" datetime="%{time}">%{time}</time>
70
+ wording: Запомни, ты делаешь Good Job тоже!
71
+ navbar:
72
+ cron_schedules: Cron
73
+ jobs: Задачи
74
+ live_poll: Живой Опрос
75
+ name: "GoodJob 👍"
76
+ processes: Процессы
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  GoodJob::Engine.routes.draw do
3
- root to: 'executions#index'
3
+ root to: redirect(path: 'jobs')
4
4
 
5
5
  resources :executions, only: %i[destroy]
6
6
 
@@ -20,7 +20,7 @@ GoodJob::Engine.routes.draw do
20
20
 
21
21
  resources :processes, only: %i[index]
22
22
 
23
- scope controller: :assets do
23
+ scope :assets, controller: :assets do
24
24
  constraints(format: :css) do
25
25
  get :bootstrap, action: :bootstrap_css
26
26
  get :style, action: :style_css
@@ -28,8 +28,10 @@ GoodJob::Engine.routes.draw do
28
28
 
29
29
  constraints(format: :js) do
30
30
  get :bootstrap, action: :bootstrap_js
31
- get :rails_ujs, action: :rails_ujs_js
32
31
  get :chartjs, action: :chartjs_js
32
+ get :rails_ujs, action: :rails_ujs_js
33
+ get :es_module_shims, action: :es_module_shims_js
34
+ get "modules/:module", action: :modules_js, as: :modules
33
35
  get :scripts, action: :scripts_js
34
36
  end
35
37
  end
@@ -28,9 +28,9 @@ module GoodJob
28
28
  # @param poll_interval [Integer, nil] sets the number of seconds between polls for jobs when +execution_mode+ is set to +:async+. You can also set this with the environment variable +GOOD_JOB_POLL_INTERVAL+. Defaults to +1+.
29
29
  # @param start_async_on_initialize [Boolean] whether to start the async scheduler when the adapter is initialized.
30
30
  def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, start_async_on_initialize: nil)
31
- if execution_mode || queues || max_threads || poll_interval || start_async_on_initialize
31
+ if queues || max_threads || poll_interval || start_async_on_initialize
32
32
  ActiveSupport::Deprecation.warn(
33
- "The GoodJob::Adapter's initialization parameters have been deprecated and will be removed in GoodJob v3. These options should be configured through GoodJob global configuration instead."
33
+ "GoodJob::Adapter's execution-related arguments (queues, max_threads, poll_interval, start_async_on_initialize) have been deprecated and will be removed in GoodJob v3. These options should be configured through GoodJob global configuration instead."
34
34
  )
35
35
  end
36
36
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.12.1'
4
+ VERSION = '2.13.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.1
4
+ version: 2.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-18 00:00:00.000000000 Z
11
+ date: 2022-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -360,11 +360,17 @@ files:
360
360
  - CHANGELOG.md
361
361
  - LICENSE.txt
362
362
  - README.md
363
+ - engine/app/assets/modules/application.js
364
+ - engine/app/assets/modules/charts.js
365
+ - engine/app/assets/modules/document_ready.js
366
+ - engine/app/assets/modules/poller.js
367
+ - engine/app/assets/modules/toasts.js
363
368
  - engine/app/assets/scripts.js
364
369
  - engine/app/assets/style.css
365
370
  - engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js
366
371
  - engine/app/assets/vendor/bootstrap/bootstrap.min.css
367
372
  - engine/app/assets/vendor/chartjs/chart.min.js
373
+ - engine/app/assets/vendor/es_module_shims.js
368
374
  - engine/app/assets/vendor/rails_ujs.js
369
375
  - engine/app/charts/good_job/scheduled_by_queue_chart.rb
370
376
  - engine/app/controllers/good_job/application_controller.rb
@@ -374,13 +380,11 @@ files:
374
380
  - engine/app/controllers/good_job/jobs_controller.rb
375
381
  - engine/app/controllers/good_job/processes_controller.rb
376
382
  - engine/app/filters/good_job/base_filter.rb
377
- - engine/app/filters/good_job/executions_filter.rb
378
383
  - engine/app/filters/good_job/jobs_filter.rb
379
384
  - engine/app/helpers/good_job/application_helper.rb
380
385
  - engine/app/views/good_job/cron_entries/index.html.erb
381
386
  - engine/app/views/good_job/cron_entries/show.html.erb
382
387
  - engine/app/views/good_job/executions/_table.erb
383
- - engine/app/views/good_job/executions/index.html.erb
384
388
  - engine/app/views/good_job/jobs/_table.erb
385
389
  - engine/app/views/good_job/jobs/index.html.erb
386
390
  - engine/app/views/good_job/jobs/show.html.erb
@@ -400,7 +404,8 @@ files:
400
404
  - engine/app/views/layouts/good_job/application.html.erb
401
405
  - engine/config/locales/en.yml
402
406
  - engine/config/locales/es.yml
403
- - engine/config/locales/ru.yaml
407
+ - engine/config/locales/nl.yml
408
+ - engine/config/locales/ru.yml
404
409
  - engine/config/routes.rb
405
410
  - engine/lib/good_job/engine.rb
406
411
  - exe/good_job
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
- module GoodJob
3
- class ExecutionsFilter < BaseFilter
4
- def states
5
- @_states ||= {
6
- 'finished' => base_query.finished.count,
7
- 'unfinished' => base_query.unfinished.count,
8
- 'running' => base_query.running.count,
9
- 'errors' => base_query.where.not(error: nil).count,
10
- }
11
- end
12
-
13
- def filtered_query
14
- query = base_query
15
- query = query.job_class(params[:job_class]) if params[:job_class].present?
16
- query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
17
- query = query.search_text(params[:query]) if params[:query].present?
18
-
19
- if params[:state]
20
- case params[:state]
21
- when 'finished'
22
- query = query.finished
23
- when 'unfinished'
24
- query = query.unfinished
25
- when 'running'
26
- query = query.running
27
- when 'errors'
28
- query = query.where.not(error: nil)
29
- end
30
- end
31
-
32
- query
33
- end
34
-
35
- private
36
-
37
- def default_base_query
38
- GoodJob::Execution.all
39
- end
40
- end
41
- end
@@ -1,19 +0,0 @@
1
- <div class="card my-3 p-6" data-gj-poll-replace id="executions-chart">
2
- <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
3
- </div>
4
-
5
- <%= render 'good_job/shared/filter', filter: @filter %>
6
-
7
- <%= render 'good_job/executions/table', executions: @filter.records %>
8
-
9
- <% if @filter.records.present? %>
10
- <nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="executions-pagination">
11
- <ul class="pagination">
12
- <li class="page-item">
13
- <%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
14
- Older executions <span aria-hidden="true">&raquo;</span>
15
- <% end %>
16
- </li>
17
- </ul>
18
- </nav>
19
- <% end %>
@@ -1,53 +0,0 @@
1
- ---
2
- ru:
3
- datetime:
4
- distance_in_words:
5
- about_x_hours:
6
- one: около 1 часа
7
- other: около %{count} часов
8
- about_x_months:
9
- one: около 1 месяца
10
- other: около %{count} месяцев
11
- about_x_years:
12
- one: около 1 года
13
- other: около %{count} лет
14
- almost_x_years:
15
- one: почти 1 год
16
- other: почти %{count} года
17
- half_a_minute: пол минуты
18
- less_than_x_minutes:
19
- one: меньше 1 минуты
20
- other: меньше %{count} минут
21
- less_than_x_seconds:
22
- one: меньше 1 секунды
23
- other: меньше %{count} секунд
24
- over_x_years:
25
- one: больше 1 года
26
- other: больше %{count} лет
27
- x_days:
28
- one: 1 день
29
- other: "%{count} дней"
30
- x_minutes:
31
- one: 1 минута
32
- other: "%{count} минут"
33
- x_months:
34
- one: 1 месяц
35
- other: "%{count} месяца"
36
- x_seconds:
37
- one: 1 секунда
38
- other: "%{count} секунд"
39
- x_years:
40
- one: 1 год
41
- other: "%{count} года"
42
- good_job:
43
- shared:
44
- footer:
45
- last_update_html: Последнее обновление <time id="page-updated-at" datetime="%{time}">%{time}</time>
46
- wording: Запомни, ты делаешь Good Job тоже!
47
- navbar:
48
- cron_schedules: Cron Расписания
49
- executions: Все Исполнения
50
- jobs: Все Задачи
51
- live_poll: Живой Опрос
52
- name: "GoodJob 👍"
53
- processes: Процессы