@cartanova/qgrid-cli 2.6.8 → 2.6.9

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 (48) hide show
  1. package/bundle/dist/application/qgrid/expired-token-reminder.js +20 -5
  2. package/bundle/dist/application/setting/setting.constant.js +226 -0
  3. package/bundle/dist/application/setting/setting.model.js +20 -2
  4. package/bundle/dist/application/setting/setting.types.js +5 -2
  5. package/bundle/dist/sonamu.config.js +2 -2
  6. package/bundle/dist/utils/providers/openai/openai-worker-pool-config.js +2 -2
  7. package/bundle/dist/utils/quiet-hours.js +30 -26
  8. package/bundle/dist/utils/slack-notify.js +6 -2
  9. package/bundle/src/application/qgrid/expired-token-reminder.test.ts +43 -0
  10. package/bundle/src/application/qgrid/expired-token-reminder.ts +22 -2
  11. package/bundle/src/application/setting/{setting.schema.test.ts → setting.constant.test.ts} +30 -1
  12. package/bundle/src/application/setting/{setting.schema.ts → setting.constant.ts} +86 -10
  13. package/bundle/src/application/setting/setting.model.ts +15 -1
  14. package/bundle/src/application/setting/setting.types.ts +3 -1
  15. package/bundle/src/application/sonamu.generated.http +7 -0
  16. package/bundle/src/sonamu.config.ts +3 -1
  17. package/bundle/src/utils/providers/openai/openai-worker-pool-config.ts +1 -1
  18. package/bundle/src/utils/quiet-hours.ts +56 -17
  19. package/bundle/src/utils/slack-notify.test.ts +96 -13
  20. package/bundle/src/utils/slack-notify.ts +8 -1
  21. package/bundle/web-dist/client/assets/{index-DrsXE13r.js → index-DnnliAzf.js} +3 -3
  22. package/bundle/web-dist/client/assets/index-rdbxJinT.css +1 -0
  23. package/bundle/web-dist/client/assets/{logs-e4-6aYnO.js → logs-5gACO5tJ.js} +1 -1
  24. package/bundle/web-dist/client/assets/{monit-eOAlYTK9.js → monit-DRCc50GU.js} +1 -1
  25. package/bundle/web-dist/client/assets/{routes-Qie4ogtJ.js → routes-DBdI9Ba1.js} +1 -1
  26. package/bundle/web-dist/client/assets/services.generated-BzwTuZRP.js +1 -0
  27. package/bundle/web-dist/client/assets/settings-B62_r1i4.js +1 -0
  28. package/bundle/web-dist/client/assets/{show-60iOOM-e.js → show-BCRrSc8-.js} +1 -1
  29. package/bundle/web-dist/client/assets/tokens-CDiXKYGf.js +1 -0
  30. package/bundle/web-dist/client/assets/{use-oauth-login-flow-0ZGq74bZ.js → use-oauth-login-flow-Qqg4koSg.js} +1 -1
  31. package/bundle/web-dist/client/index.html +2 -2
  32. package/bundle/web-dist/server/assets/{logs-DmkzGxGU.js → logs-C1rxLh8H.js} +1 -1
  33. package/bundle/web-dist/server/assets/{monit-DyKLJX4s.js → monit-C8gx9Sar.js} +1 -1
  34. package/bundle/web-dist/server/assets/{routes-g62E9xmC.js → routes-ZW_Ih5Em.js} +8 -5
  35. package/bundle/web-dist/server/assets/{services.generated-DADr9OGH.js → services.generated-CZIChgHV.js} +8 -0
  36. package/bundle/web-dist/server/assets/{settings-CMJKayff.js → settings--aoatERH.js} +72 -22
  37. package/bundle/web-dist/server/assets/{show-BGAiCPGq.js → show-BY27w9oi.js} +1 -1
  38. package/bundle/web-dist/server/assets/{tokens-DmhIfVct.js → tokens-DIObtTuZ.js} +13 -53
  39. package/bundle/web-dist/server/assets/{use-oauth-login-flow-B6MY65IN.js → use-oauth-login-flow-V3k8PSqQ.js} +1 -1
  40. package/bundle/web-dist/server/entry-server.generated.js +7 -7
  41. package/package.json +1 -1
  42. package/skills/qgrid/references/cli-env-and-server-boot.md +5 -2
  43. package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -1
  44. package/bundle/dist/application/setting/setting.schema.js +0 -157
  45. package/bundle/web-dist/client/assets/index-7SZyiqAx.css +0 -1
  46. package/bundle/web-dist/client/assets/services.generated-T4ctVcia.js +0 -1
  47. package/bundle/web-dist/client/assets/settings-CTVZpAAg.js +0 -1
  48. package/bundle/web-dist/client/assets/tokens-DdDccGlX.js +0 -1
@@ -1,4 +1,4 @@
1
- import { SETTING_DEFS } from "../../../application/setting/setting.schema.js";
1
+ import { SETTING_DEFS } from "../../../application/setting/setting.constant.js";
2
2
  import { getSetting } from "../../../application/setting/setting.store.js";
3
3
  //#region src/utils/providers/openai/openai-worker-pool-config.ts
4
4
  const MAX_OPENAI_WORKERS_PER_TOKEN = 20;
@@ -48,4 +48,4 @@ function estimateOpenAIWorkerRssGiB(totalWorkers) {
48
48
  //#endregion
49
49
  export { MAX_OPENAI_WORKERS_PER_TOKEN, OPENAI_WORKER_BASE_RSS_GIB, OPENAI_WORKER_RSS_GIB, estimateOpenAIWorkerRssGiB, resolveOpenAIWorkerPoolConfig };
50
50
 
51
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXdvcmtlci1wb29sLWNvbmZpZy5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvdXRpbHMvcHJvdmlkZXJzL29wZW5haS9vcGVuYWktd29ya2VyLXBvb2wtY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIE1BWF9PUEVOQUlfRVNUSU1BVEVEX1JTU19HSUIsXG4gIE1BWF9PUEVOQUlfTUlOX0hPU1RfQVZBSUxBQkxFX0dJQixcbiAgU0VUVElOR19ERUZTLFxufSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vc2V0dGluZy9zZXR0aW5nLnNjaGVtYVwiO1xuaW1wb3J0IHsgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi8uLi8uLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcblxuZXhwb3J0IGNvbnN0IE1BWF9PUEVOQUlfV09SS0VSU19QRVJfVE9LRU4gPSAyMDtcbmNvbnN0IERFRkFVTFRfT1BFTkFJX01JTl9XT1JLRVJTX1BFUl9UT0tFTiA9IDE7XG5jb25zdCBERUZBVUxUX09QRU5BSV9NQVhfV09SS0VSU19QRVJfVE9LRU4gPSAzO1xuXG5leHBvcnQgY29uc3QgT1BFTkFJX1dPUktFUl9CQVNFX1JTU19HSUIgPSAwLjcxO1xuZXhwb3J0IGNvbnN0IE9QRU5BSV9XT1JLRVJfUlNTX0dJQiA9IDAuMTU3O1xuXG5leHBvcnQgdHlwZSBPcGVuQUlXb3JrZXJQb29sQ29uZmlnID0ge1xuICBhdXRvc2NhbGU6IGJvb2xlYW47XG4gIG1pbldvcmtlcnNQZXJUb2tlbjogbnVtYmVyO1xuICBtYXhXb3JrZXJzUGVyVG9rZW46IG51bWJlcjtcbiAgc2NhbGVJbnRlcnZhbE1zOiBudW1iZXI7XG4gIHNjYWxlRG93bklkbGVNczogbnVtYmVyO1xuICBtYXhFc3RpbWF0ZWRSc3NHaUI6IG51bWJlcjtcbiAgbWluSG9zdEF2YWlsYWJsZUdpQjogbnVtYmVyO1xufTtcblxuZnVuY3Rpb24gYm91bmRlZEludGVnZXIoXG4gIHZhbHVlOiBzdHJpbmcgfCB1bmRlZmluZWQsXG4gIGZhbGxiYWNrOiBudW1iZXIsXG4gIG1pbjogbnVtYmVyLFxuICBtYXg6IG51bWJlcixcbik6IG51bWJlciB7XG4gIGNvbnN0IHBhcnNlZCA9IE51bWJlcih2YWx1ZSk7XG4gIGlmICghTnVtYmVyLmlzRmluaXRlKHBhcnNlZCkpIHJldHVybiBmYWxsYmFjaztcbiAgcmV0dXJuIE1hdGgubWF4KG1pbiwgTWF0aC5taW4oTWF0aC5mbG9vcihwYXJzZWQpLCBtYXgpKTtcbn1cblxuZnVuY3Rpb24gYm91bmRlZE51bWJlcihcbiAgdmFsdWU6IHN0cmluZyB8IHVuZGVmaW5lZCxcbiAgZmFsbGJhY2s6IG51bWJlcixcbiAgbWluOiBudW1iZXIsXG4gIG1heDogbnVtYmVyLFxuKTogbnVtYmVyIHtcbiAgY29uc3QgcGFyc2VkID0gTnVtYmVyKHZhbHVlKTtcbiAgaWYgKCFOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSkgcmV0dXJuIGZhbGxiYWNrO1xuICByZXR1cm4gTWF0aC5tYXgobWluLCBNYXRoLm1pbihwYXJzZWQsIG1heCkpO1xufVxuXG4vKipcbiAqIOyEpOyglSDsoIDsnqXshowoREIpIOulvCDrqLzsoIAg67O06rOgIOyXhuycvOuptCBlbnYg66GcIOuWqOyWtOyngOuKlCDsobDtmowg6rCd7LK0LlxuICpcbiAqIOq4sOyhtCDtjIzsi7Eg66Gc7KeB7J20IGBlbnZbS0VZXWAg7ZiV7YOc66W8IOyghOygnO2VmOuvgOuhnCwg7IaM7Iqk66eMIOuwlOq/lCDrgbzsmrDqs6Ag6rKA7KadwrfquLDrs7jqsJIg7LKY66as64qUXG4gKiDqt7jrjIDroZwg65GU64ukLiBgZ2V0YCDsoJHqt7zrp4wg7KeA7JuQ7ZWY66+A66GcIGBpbmDsnbTrgpggYE9iamVjdC5rZXlzKClg66GcIOyhsO2ajO2VmOyngCDslYrripTri6QuIO2FjOyKpO2KuOuKlFxuICogYGVudmAg7J247J6Q7JeQIOyInOyImO2VnCDrp7XsnYQg64SY6rKoIOyggOyepeyGjOulvCDsmrDtmoztlZzri6QuXG4gKi9cbmZ1bmN0aW9uIHNldHRpbmdzQmFja2VkRW52KCk6IFJlY29yZDxzdHJpbmcsIHN0cmluZyB8IHVuZGVmaW5lZD4ge1xuICByZXR1cm4gbmV3IFByb3h5KHt9IGFzIFJlY29yZDxzdHJpbmcsIHN0cmluZyB8IHVuZGVmaW5lZD4sIHtcbiAgICBnZXQ6IChfdGFyZ2V0LCBwcm9wOiBzdHJpbmcpID0+IHtcbiAgICAgIGNvbnN0IGRlZiA9IFNFVFRJTkdfREVGUy5maW5kKChkKSA9PiBkLmVudktleSA9PT0gcHJvcCk7XG4gICAgICByZXR1cm4gZGVmID8gZ2V0U2V0dGluZyhkZWYua2V5LCBkZWYuZW52S2V5KSA6IHByb2Nlc3MuZW52W3Byb3BdO1xuICAgIH0sXG4gIH0pO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVzb2x2ZU9wZW5BSVdvcmtlclBvb2xDb25maWcoXG4gIGVudjogUmVjb3JkPHN0cmluZywgc3RyaW5nIHwgdW5kZWZpbmVkPiA9IHNldHRpbmdzQmFja2VkRW52KCksXG4pOiBPcGVuQUlXb3JrZXJQb29sQ29uZmlnIHtcbiAgY29uc3QgYXV0b3NjYWxlID0gZW52LlFHUklEX09QRU5BSV9BVVRPU0NBTEUgIT09IFwiZmFsc2VcIiAmJiBlbnYuUUdSSURfT1BFTkFJX0FVVE9TQ0FMRSAhPT0gXCIwXCI7XG4gIGNvbnN0IG1pbldvcmtlcnNQZXJUb2tlbiA9IGJvdW5kZWRJbnRlZ2VyKFxuICAgIGVudi5RR1JJRF9PUEVOQUlfTUlOX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgIERFRkFVTFRfT1BFTkFJX01JTl9XT1JLRVJTX1BFUl9UT0tFTixcbiAgICAxLFxuICAgIE1BWF9PUEVOQUlfV09SS0VSU19QRVJfVE9LRU4sXG4gICk7XG4gIHJldHVybiB7XG4gICAgYXV0b3NjYWxlLFxuICAgIG1pbldvcmtlcnNQZXJUb2tlbixcbiAgICAvLyBhdXRvc2NhbGUg7J20IOq6vOyngOuptCBtYXgg64qUIG1pbiDsnLzroZwg6rOg7KCV65CY66+A66GcIGVudiDtjIzsi7Eg7J6Q7LK06rCAIGF1dG9zY2FsZSDrtoTquLAg7JWI7JeQ66eMIOyeiOuLpC5cbiAgICBtYXhXb3JrZXJzUGVyVG9rZW46IGF1dG9zY2FsZVxuICAgICAgPyBNYXRoLm1heChcbiAgICAgICAgICBtaW5Xb3JrZXJzUGVyVG9rZW4sXG4gICAgICAgICAgYm91bmRlZEludGVnZXIoXG4gICAgICAgICAgICBlbnYuUUdSSURfT1BFTkFJX01BWF9XT1JLRVJTX1BFUl9UT0tFTixcbiAgICAgICAgICAgIERFRkFVTFRfT1BFTkFJX01BWF9XT1JLRVJTX1BFUl9UT0tFTixcbiAgICAgICAgICAgIDEsXG4gICAgICAgICAgICBNQVhfT1BFTkFJX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgICAgICAgICksXG4gICAgICAgIClcbiAgICAgIDogbWluV29ya2Vyc1BlclRva2VuLFxuICAgIHNjYWxlSW50ZXJ2YWxNczogYm91bmRlZEludGVnZXIoZW52LlFHUklEX09QRU5BSV9TQ0FMRV9JTlRFUlZBTF9NUywgNV8wMDAsIDI1MCwgMzAwXzAwMCksXG4gICAgc2NhbGVEb3duSWRsZU1zOiBib3VuZGVkSW50ZWdlcihcbiAgICAgIGVudi5RR1JJRF9PUEVOQUlfU0NBTEVfRE9XTl9JRExFX01TLFxuICAgICAgMTAgKiA2MF8wMDAsXG4gICAgICAxXzAwMCxcbiAgICAgIDI0ICogNjAgKiA2MF8wMDAsXG4gICAgKSxcbiAgICBtYXhFc3RpbWF0ZWRSc3NHaUI6IGJvdW5kZWROdW1iZXIoXG4gICAgICBlbnYuUUdSSURfT1BFTkFJX01BWF9FU1RJTUFURURfUlNTX0dJQixcbiAgICAgIDE2LFxuICAgICAgMSxcbiAgICAgIE1BWF9PUEVOQUlfRVNUSU1BVEVEX1JTU19HSUIsXG4gICAgKSxcbiAgICBtaW5Ib3N0QXZhaWxhYmxlR2lCOiBib3VuZGVkTnVtYmVyKFxuICAgICAgZW52LlFHUklEX09QRU5BSV9NSU5fSE9TVF9BVkFJTEFCTEVfR0lCLFxuICAgICAgMjAsXG4gICAgICAwLFxuICAgICAgTUFYX09QRU5BSV9NSU5fSE9TVF9BVkFJTEFCTEVfR0lCLFxuICAgICksXG4gIH07XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBlc3RpbWF0ZU9wZW5BSVdvcmtlclJzc0dpQih0b3RhbFdvcmtlcnM6IG51bWJlcik6IG51bWJlciB7XG4gIHJldHVybiBPUEVOQUlfV09SS0VSX0JBU0VfUlNTX0dJQiArIE9QRU5BSV9XT1JLRVJfUlNTX0dJQiAqIHRvdGFsV29ya2Vycztcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7O0FBT0EsTUFBYSwrQkFBK0I7QUFDNUMsTUFBTSx1Q0FBdUM7QUFDN0MsTUFBTSx1Q0FBdUM7QUFFN0MsTUFBYSw2QkFBNkI7QUFDMUMsTUFBYSx3QkFBd0I7QUFZckMsU0FBUyxlQUNQLE9BQ0EsVUFDQSxLQUNBLEtBQ1E7Q0FDUixNQUFNLFNBQVMsT0FBTyxLQUFLO0NBQzNCLElBQUksQ0FBQyxPQUFPLFNBQVMsTUFBTSxHQUFHLE9BQU87Q0FDckMsT0FBTyxLQUFLLElBQUksS0FBSyxLQUFLLElBQUksS0FBSyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUM7QUFDeEQ7QUFFQSxTQUFTLGNBQ1AsT0FDQSxVQUNBLEtBQ0EsS0FDUTtDQUNSLE1BQU0sU0FBUyxPQUFPLEtBQUs7Q0FDM0IsSUFBSSxDQUFDLE9BQU8sU0FBUyxNQUFNLEdBQUcsT0FBTztDQUNyQyxPQUFPLEtBQUssSUFBSSxLQUFLLEtBQUssSUFBSSxRQUFRLEdBQUcsQ0FBQztBQUM1Qzs7Ozs7Ozs7QUFTQSxTQUFTLG9CQUF3RDtDQUMvRCxPQUFPLElBQUksTUFBTSxDQUFDLEdBQXlDLEVBQ3pELE1BQU0sU0FBUyxTQUFpQjtFQUM5QixNQUFNLE1BQU0sYUFBYSxNQUFNLE1BQU0sRUFBRSxXQUFXLElBQUk7RUFDdEQsT0FBTyxNQUFNLFdBQVcsSUFBSSxLQUFLLElBQUksTUFBTSxJQUFJLFFBQVEsSUFBSTtDQUM3RCxFQUNGLENBQUM7QUFDSDtBQUVBLFNBQWdCLDhCQUNkLE1BQTBDLGtCQUFrQixHQUNwQztDQUN4QixNQUFNLFlBQVksSUFBSSwyQkFBMkIsV0FBVyxJQUFJLDJCQUEyQjtDQUMzRixNQUFNLHFCQUFxQixlQUN6QixJQUFJLG9DQUNKLHNDQUNBLEdBQUEsRUFFRjtDQUNBLE9BQU87RUFDTDtFQUNBO0VBRUEsb0JBQW9CLFlBQ2hCLEtBQUssSUFDSCxvQkFDQSxlQUNFLElBQUksb0NBQ0osc0NBQ0EsR0FBQSxFQUVGLENBQ0YsSUFDQTtFQUNKLGlCQUFpQixlQUFlLElBQUksZ0NBQWdDLEtBQU8sS0FBSyxHQUFPO0VBQ3ZGLGlCQUFpQixlQUNmLElBQUksaUNBQ0osS0FBSyxLQUNMLEtBQ0EsT0FBVSxHQUNaO0VBQ0Esb0JBQW9CLGNBQ2xCLElBQUksb0NBQ0osSUFDQSxHQUFBLEVBRUY7RUFDQSxxQkFBcUIsY0FDbkIsSUFBSSxxQ0FDSixJQUNBLEdBQUEsRUFFRjtDQUNGO0FBQ0Y7QUFFQSxTQUFnQiwyQkFBMkIsY0FBOEI7Q0FDdkUsT0FBTyw2QkFBNkIsd0JBQXdCO0FBQzlEIn0=
51
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXdvcmtlci1wb29sLWNvbmZpZy5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvdXRpbHMvcHJvdmlkZXJzL29wZW5haS9vcGVuYWktd29ya2VyLXBvb2wtY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIE1BWF9PUEVOQUlfRVNUSU1BVEVEX1JTU19HSUIsXG4gIE1BWF9PUEVOQUlfTUlOX0hPU1RfQVZBSUxBQkxFX0dJQixcbiAgU0VUVElOR19ERUZTLFxufSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vc2V0dGluZy9zZXR0aW5nLmNvbnN0YW50XCI7XG5pbXBvcnQgeyBnZXRTZXR0aW5nIH0gZnJvbSBcIi4uLy4uLy4uL2FwcGxpY2F0aW9uL3NldHRpbmcvc2V0dGluZy5zdG9yZVwiO1xuXG5leHBvcnQgY29uc3QgTUFYX09QRU5BSV9XT1JLRVJTX1BFUl9UT0tFTiA9IDIwO1xuY29uc3QgREVGQVVMVF9PUEVOQUlfTUlOX1dPUktFUlNfUEVSX1RPS0VOID0gMTtcbmNvbnN0IERFRkFVTFRfT1BFTkFJX01BWF9XT1JLRVJTX1BFUl9UT0tFTiA9IDM7XG5cbmV4cG9ydCBjb25zdCBPUEVOQUlfV09SS0VSX0JBU0VfUlNTX0dJQiA9IDAuNzE7XG5leHBvcnQgY29uc3QgT1BFTkFJX1dPUktFUl9SU1NfR0lCID0gMC4xNTc7XG5cbmV4cG9ydCB0eXBlIE9wZW5BSVdvcmtlclBvb2xDb25maWcgPSB7XG4gIGF1dG9zY2FsZTogYm9vbGVhbjtcbiAgbWluV29ya2Vyc1BlclRva2VuOiBudW1iZXI7XG4gIG1heFdvcmtlcnNQZXJUb2tlbjogbnVtYmVyO1xuICBzY2FsZUludGVydmFsTXM6IG51bWJlcjtcbiAgc2NhbGVEb3duSWRsZU1zOiBudW1iZXI7XG4gIG1heEVzdGltYXRlZFJzc0dpQjogbnVtYmVyO1xuICBtaW5Ib3N0QXZhaWxhYmxlR2lCOiBudW1iZXI7XG59O1xuXG5mdW5jdGlvbiBib3VuZGVkSW50ZWdlcihcbiAgdmFsdWU6IHN0cmluZyB8IHVuZGVmaW5lZCxcbiAgZmFsbGJhY2s6IG51bWJlcixcbiAgbWluOiBudW1iZXIsXG4gIG1heDogbnVtYmVyLFxuKTogbnVtYmVyIHtcbiAgY29uc3QgcGFyc2VkID0gTnVtYmVyKHZhbHVlKTtcbiAgaWYgKCFOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSkgcmV0dXJuIGZhbGxiYWNrO1xuICByZXR1cm4gTWF0aC5tYXgobWluLCBNYXRoLm1pbihNYXRoLmZsb29yKHBhcnNlZCksIG1heCkpO1xufVxuXG5mdW5jdGlvbiBib3VuZGVkTnVtYmVyKFxuICB2YWx1ZTogc3RyaW5nIHwgdW5kZWZpbmVkLFxuICBmYWxsYmFjazogbnVtYmVyLFxuICBtaW46IG51bWJlcixcbiAgbWF4OiBudW1iZXIsXG4pOiBudW1iZXIge1xuICBjb25zdCBwYXJzZWQgPSBOdW1iZXIodmFsdWUpO1xuICBpZiAoIU51bWJlci5pc0Zpbml0ZShwYXJzZWQpKSByZXR1cm4gZmFsbGJhY2s7XG4gIHJldHVybiBNYXRoLm1heChtaW4sIE1hdGgubWluKHBhcnNlZCwgbWF4KSk7XG59XG5cbi8qKlxuICog7ISk7KCVIOyggOyepeyGjChEQikg66W8IOuovOyggCDrs7Tqs6Ag7JeG7Jy866m0IGVudiDroZwg65ao7Ja07KeA64qUIOyhsO2ajCDqsJ3ssrQuXG4gKlxuICog6riw7KG0IO2MjOyLsSDroZzsp4HsnbQgYGVudltLRVldYCDtmJXtg5zrpbwg7KCE7KCc7ZWY66+A66GcLCDshozsiqTrp4wg67CU6r+UIOuBvOyasOqzoCDqsoDspp3Ct+q4sOuzuOqwkiDsspjrpqzripRcbiAqIOq3uOuMgOuhnCDrkZTri6QuIGBnZXRgIOygkeq3vOunjCDsp4Dsm5DtlZjrr4DroZwgYGluYOydtOuCmCBgT2JqZWN0LmtleXMoKWDroZwg7KGw7ZqM7ZWY7KeAIOyViuuKlOuLpC4g7YWM7Iqk7Yq464qUXG4gKiBgZW52YCDsnbjsnpDsl5Ag7Iic7IiY7ZWcIOunteydhCDrhJjqsqgg7KCA7J6l7IaM66W8IOyasO2ajO2VnOuLpC5cbiAqL1xuZnVuY3Rpb24gc2V0dGluZ3NCYWNrZWRFbnYoKTogUmVjb3JkPHN0cmluZywgc3RyaW5nIHwgdW5kZWZpbmVkPiB7XG4gIHJldHVybiBuZXcgUHJveHkoe30gYXMgUmVjb3JkPHN0cmluZywgc3RyaW5nIHwgdW5kZWZpbmVkPiwge1xuICAgIGdldDogKF90YXJnZXQsIHByb3A6IHN0cmluZykgPT4ge1xuICAgICAgY29uc3QgZGVmID0gU0VUVElOR19ERUZTLmZpbmQoKGQpID0+IGQuZW52S2V5ID09PSBwcm9wKTtcbiAgICAgIHJldHVybiBkZWYgPyBnZXRTZXR0aW5nKGRlZi5rZXksIGRlZi5lbnZLZXkpIDogcHJvY2Vzcy5lbnZbcHJvcF07XG4gICAgfSxcbiAgfSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZXNvbHZlT3BlbkFJV29ya2VyUG9vbENvbmZpZyhcbiAgZW52OiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmcgfCB1bmRlZmluZWQ+ID0gc2V0dGluZ3NCYWNrZWRFbnYoKSxcbik6IE9wZW5BSVdvcmtlclBvb2xDb25maWcge1xuICBjb25zdCBhdXRvc2NhbGUgPSBlbnYuUUdSSURfT1BFTkFJX0FVVE9TQ0FMRSAhPT0gXCJmYWxzZVwiICYmIGVudi5RR1JJRF9PUEVOQUlfQVVUT1NDQUxFICE9PSBcIjBcIjtcbiAgY29uc3QgbWluV29ya2Vyc1BlclRva2VuID0gYm91bmRlZEludGVnZXIoXG4gICAgZW52LlFHUklEX09QRU5BSV9NSU5fV09SS0VSU19QRVJfVE9LRU4sXG4gICAgREVGQVVMVF9PUEVOQUlfTUlOX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgIDEsXG4gICAgTUFYX09QRU5BSV9XT1JLRVJTX1BFUl9UT0tFTixcbiAgKTtcbiAgcmV0dXJuIHtcbiAgICBhdXRvc2NhbGUsXG4gICAgbWluV29ya2Vyc1BlclRva2VuLFxuICAgIC8vIGF1dG9zY2FsZSDsnbQg6rq87KeA66m0IG1heCDripQgbWluIOycvOuhnCDqs6DsoJXrkJjrr4DroZwgZW52IO2MjOyLsSDsnpDssrTqsIAgYXV0b3NjYWxlIOu2hOq4sCDslYjsl5Drp4wg7J6I64ukLlxuICAgIG1heFdvcmtlcnNQZXJUb2tlbjogYXV0b3NjYWxlXG4gICAgICA/IE1hdGgubWF4KFxuICAgICAgICAgIG1pbldvcmtlcnNQZXJUb2tlbixcbiAgICAgICAgICBib3VuZGVkSW50ZWdlcihcbiAgICAgICAgICAgIGVudi5RR1JJRF9PUEVOQUlfTUFYX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgICAgICAgICAgREVGQVVMVF9PUEVOQUlfTUFYX1dPUktFUlNfUEVSX1RPS0VOLFxuICAgICAgICAgICAgMSxcbiAgICAgICAgICAgIE1BWF9PUEVOQUlfV09SS0VSU19QRVJfVE9LRU4sXG4gICAgICAgICAgKSxcbiAgICAgICAgKVxuICAgICAgOiBtaW5Xb3JrZXJzUGVyVG9rZW4sXG4gICAgc2NhbGVJbnRlcnZhbE1zOiBib3VuZGVkSW50ZWdlcihlbnYuUUdSSURfT1BFTkFJX1NDQUxFX0lOVEVSVkFMX01TLCA1XzAwMCwgMjUwLCAzMDBfMDAwKSxcbiAgICBzY2FsZURvd25JZGxlTXM6IGJvdW5kZWRJbnRlZ2VyKFxuICAgICAgZW52LlFHUklEX09QRU5BSV9TQ0FMRV9ET1dOX0lETEVfTVMsXG4gICAgICAxMCAqIDYwXzAwMCxcbiAgICAgIDFfMDAwLFxuICAgICAgMjQgKiA2MCAqIDYwXzAwMCxcbiAgICApLFxuICAgIG1heEVzdGltYXRlZFJzc0dpQjogYm91bmRlZE51bWJlcihcbiAgICAgIGVudi5RR1JJRF9PUEVOQUlfTUFYX0VTVElNQVRFRF9SU1NfR0lCLFxuICAgICAgMTYsXG4gICAgICAxLFxuICAgICAgTUFYX09QRU5BSV9FU1RJTUFURURfUlNTX0dJQixcbiAgICApLFxuICAgIG1pbkhvc3RBdmFpbGFibGVHaUI6IGJvdW5kZWROdW1iZXIoXG4gICAgICBlbnYuUUdSSURfT1BFTkFJX01JTl9IT1NUX0FWQUlMQUJMRV9HSUIsXG4gICAgICAyMCxcbiAgICAgIDAsXG4gICAgICBNQVhfT1BFTkFJX01JTl9IT1NUX0FWQUlMQUJMRV9HSUIsXG4gICAgKSxcbiAgfTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGVzdGltYXRlT3BlbkFJV29ya2VyUnNzR2lCKHRvdGFsV29ya2VyczogbnVtYmVyKTogbnVtYmVyIHtcbiAgcmV0dXJuIE9QRU5BSV9XT1JLRVJfQkFTRV9SU1NfR0lCICsgT1BFTkFJX1dPUktFUl9SU1NfR0lCICogdG90YWxXb3JrZXJzO1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7QUFPQSxNQUFhLCtCQUErQjtBQUM1QyxNQUFNLHVDQUF1QztBQUM3QyxNQUFNLHVDQUF1QztBQUU3QyxNQUFhLDZCQUE2QjtBQUMxQyxNQUFhLHdCQUF3QjtBQVlyQyxTQUFTLGVBQ1AsT0FDQSxVQUNBLEtBQ0EsS0FDUTtDQUNSLE1BQU0sU0FBUyxPQUFPLEtBQUs7Q0FDM0IsSUFBSSxDQUFDLE9BQU8sU0FBUyxNQUFNLEdBQUcsT0FBTztDQUNyQyxPQUFPLEtBQUssSUFBSSxLQUFLLEtBQUssSUFBSSxLQUFLLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQztBQUN4RDtBQUVBLFNBQVMsY0FDUCxPQUNBLFVBQ0EsS0FDQSxLQUNRO0NBQ1IsTUFBTSxTQUFTLE9BQU8sS0FBSztDQUMzQixJQUFJLENBQUMsT0FBTyxTQUFTLE1BQU0sR0FBRyxPQUFPO0NBQ3JDLE9BQU8sS0FBSyxJQUFJLEtBQUssS0FBSyxJQUFJLFFBQVEsR0FBRyxDQUFDO0FBQzVDOzs7Ozs7OztBQVNBLFNBQVMsb0JBQXdEO0NBQy9ELE9BQU8sSUFBSSxNQUFNLENBQUMsR0FBeUMsRUFDekQsTUFBTSxTQUFTLFNBQWlCO0VBQzlCLE1BQU0sTUFBTSxhQUFhLE1BQU0sTUFBTSxFQUFFLFdBQVcsSUFBSTtFQUN0RCxPQUFPLE1BQU0sV0FBVyxJQUFJLEtBQUssSUFBSSxNQUFNLElBQUksUUFBUSxJQUFJO0NBQzdELEVBQ0YsQ0FBQztBQUNIO0FBRUEsU0FBZ0IsOEJBQ2QsTUFBMEMsa0JBQWtCLEdBQ3BDO0NBQ3hCLE1BQU0sWUFBWSxJQUFJLDJCQUEyQixXQUFXLElBQUksMkJBQTJCO0NBQzNGLE1BQU0scUJBQXFCLGVBQ3pCLElBQUksb0NBQ0osc0NBQ0EsR0FBQSxFQUVGO0NBQ0EsT0FBTztFQUNMO0VBQ0E7RUFFQSxvQkFBb0IsWUFDaEIsS0FBSyxJQUNILG9CQUNBLGVBQ0UsSUFBSSxvQ0FDSixzQ0FDQSxHQUFBLEVBRUYsQ0FDRixJQUNBO0VBQ0osaUJBQWlCLGVBQWUsSUFBSSxnQ0FBZ0MsS0FBTyxLQUFLLEdBQU87RUFDdkYsaUJBQWlCLGVBQ2YsSUFBSSxpQ0FDSixLQUFLLEtBQ0wsS0FDQSxPQUFVLEdBQ1o7RUFDQSxvQkFBb0IsY0FDbEIsSUFBSSxvQ0FDSixJQUNBLEdBQUEsRUFFRjtFQUNBLHFCQUFxQixjQUNuQixJQUFJLHFDQUNKLElBQ0EsR0FBQSxFQUVGO0NBQ0Y7QUFDRjtBQUVBLFNBQWdCLDJCQUEyQixjQUE4QjtDQUN2RSxPQUFPLDZCQUE2Qix3QkFBd0I7QUFDOUQifQ==
@@ -1,29 +1,19 @@
1
1
  //#region src/utils/quiet-hours.ts
2
- /**
3
- * Slack 알림 조용 시간.
4
- *
5
- * 토큰 만료는 사람이 재로그인해야 풀리는데, 그 사람이 자거나 쉬는 동안 알려도 할 수 있는
6
- * 일이 없다. 평일 업무 시간에만 알리고 나머지는 다음 근무 시간으로 미룬다 — 만료 알림은
7
- * 주기적으로 반복되므로 조용 시간에 버려도 아침 첫 주기에 다시 온다.
8
- *
9
- * 다만 provider 의 마지막 토큰이 죽는 것은 서비스 정지 신호라 이 규칙에서 제외한다
10
- * (호출부가 `notifySlack` 을 그냥 호출하면 된다).
11
- */
12
- /** 이 시각부터(포함) 조용해진다. */
13
- const QUIET_FROM_HOUR = 20;
14
- /** 이 시각부터(포함) 다시 알린다. */
15
- const QUIET_UNTIL_HOUR = 8;
2
+ /** `setting.constant.ts` 의 두 fallback 과 같아야 한다. 일치 여부는 테스트가 지킨다. */
3
+ const DEFAULT_QUIET_FROM_HOUR = 20;
4
+ const DEFAULT_QUIET_UNTIL_HOUR = 8;
16
5
  /**
17
6
  * 서버가 UTC 로 돌아도 한국 시간으로 판정한다. dev0 는 UTC 이므로 `getHours()` 를 그대로
18
7
  * 쓰면 "저녁 8시"가 한국 시간 새벽 5시가 된다.
19
8
  */
9
+ const SEOUL_FORMATTER = new Intl.DateTimeFormat("en-US", {
10
+ timeZone: "Asia/Seoul",
11
+ hour: "numeric",
12
+ hour12: false,
13
+ weekday: "short"
14
+ });
20
15
  function seoulParts(at) {
21
- const parts = new Intl.DateTimeFormat("en-US", {
22
- timeZone: "Asia/Seoul",
23
- hour: "numeric",
24
- hour12: false,
25
- weekday: "short"
26
- }).formatToParts(at);
16
+ const parts = SEOUL_FORMATTER.formatToParts(at);
27
17
  const hour = Number(parts.find((p) => p.type === "hour")?.value ?? "0");
28
18
  const weekday = parts.find((p) => p.type === "weekday")?.value ?? "";
29
19
  return {
@@ -31,13 +21,27 @@ function seoulParts(at) {
31
21
  weekday
32
22
  };
33
23
  }
34
- /** 주말이거나 20시~익일 8시면 조용 시간. */
35
- function isQuietHours(at = /* @__PURE__ */ new Date()) {
24
+ /** 0..23 범위를 벗어나거나 숫자가 아니면 기본값으로 떨어진다. */
25
+ function boundedHour(raw, fallback) {
26
+ const parsed = Number(raw);
27
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 23) return fallback;
28
+ return parsed;
29
+ }
30
+ /**
31
+ * 조용 시간인지 판정한다.
32
+ *
33
+ * 시작 > 종료면 자정을 넘는 구간(20시~익일 8시), 시작 < 종료면 같은 날 구간(0시~8시처럼)
34
+ * 으로 읽는다. 야간 근무처럼 낮에 조용하고 싶은 팀도 있어 뒤집힌 값을 막지 않는다.
35
+ */
36
+ function isQuietHours(at, readSetting) {
36
37
  const { hour, weekday } = seoulParts(at);
37
- if (weekday === "Sat" || weekday === "Sun") return true;
38
- return hour >= QUIET_FROM_HOUR || hour < QUIET_UNTIL_HOUR;
38
+ if ((weekday === "Sat" || weekday === "Sun") && readSetting("slack.notifyOnWeekends", "SLACK_NOTIFY_ON_WEEKENDS") !== "true") return true;
39
+ const from = boundedHour(readSetting("slack.quietFromHour", "SLACK_QUIET_FROM_HOUR"), 20);
40
+ const until = boundedHour(readSetting("slack.quietUntilHour", "SLACK_QUIET_UNTIL_HOUR"), 8);
41
+ if (from === until) return false;
42
+ return from > until ? hour >= from || hour < until : hour >= from && hour < until;
39
43
  }
40
44
  //#endregion
41
- export { isQuietHours };
45
+ export { DEFAULT_QUIET_FROM_HOUR, DEFAULT_QUIET_UNTIL_HOUR, isQuietHours };
42
46
 
43
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVpZXQtaG91cnMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3F1aWV0LWhvdXJzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogU2xhY2sg7JWM66a8IOyhsOyaqSDsi5zqsIQuXG4gKlxuICog7Yag7YGwIOunjOujjOuKlCDsgqzrnozsnbQg7J6s66Gc6re47J247ZW07JW8IO2SgOumrOuKlOuNsCwg6re4IOyCrOuejOydtCDsnpDqsbDrgpgg7Ims64qUIOuPmeyViCDslYzroKTrj4Qg7ZWgIOyImCDsnojripRcbiAqIOydvOydtCDsl4bri6QuIO2PieydvCDsl4XrrLQg7Iuc6rCE7JeQ66eMIOyVjOumrOqzoCDrgpjrqLjsp4DripQg64uk7J2MIOq3vOustCDsi5zqsITsnLzroZwg66+466Os64ukIOKAlCDrp4zro4wg7JWM66a87J2AXG4gKiDso7zquLDsoIHsnLzroZwg67CY67O165CY66+A66GcIOyhsOyaqSDsi5zqsITsl5Ag67KE66Ck64+EIOyVhOy5qCDssqsg7KO86riw7JeQIOuLpOyLnCDsmKjri6QuXG4gKlxuICog64uk66eMIHByb3ZpZGVyIOydmCDrp4jsp4Drp4kg7Yag7YGw7J20IOyjveuKlCDqsoPsnYAg7ISc67mE7IqkIOygleyngCDsi6DtmLjrnbwg7J20IOq3nOy5meyXkOyEnCDsoJzsmbjtlZzri6RcbiAqICjtmLjstpzrtoDqsIAgYG5vdGlmeVNsYWNrYCDsnYQg6re464OlIO2YuOy2nO2VmOuptCDrkJzri6QpLlxuICovXG5cbi8qKiDsnbQg7Iuc6rCB67aA7YSwKO2PrO2VqCkg7KGw7Jqp7ZW07KeE64ukLiAqL1xuY29uc3QgUVVJRVRfRlJPTV9IT1VSID0gMjA7XG4vKiog7J20IOyLnOqwgeu2gO2EsCjtj6ztlagpIOuLpOyLnCDslYzrprDri6QuICovXG5jb25zdCBRVUlFVF9VTlRJTF9IT1VSID0gODtcblxuLyoqXG4gKiDshJzrsoTqsIAgVVRDIOuhnCDrj4zslYTrj4Qg7ZWc6rWtIOyLnOqwhOycvOuhnCDtjJDsoJXtlZzri6QuIGRldjAg64qUIFVUQyDsnbTrr4DroZwgYGdldEhvdXJzKClgIOulvCDqt7jrjIDroZxcbiAqIOyTsOuptCBcIuyggOuFgSA47IucXCLqsIAg7ZWc6rWtIOyLnOqwhCDsg4jrsr0gNeyLnOqwgCDrkJzri6QuXG4gKi9cbmZ1bmN0aW9uIHNlb3VsUGFydHMoYXQ6IERhdGUpOiB7IGhvdXI6IG51bWJlcjsgd2Vla2RheTogc3RyaW5nIH0ge1xuICBjb25zdCBmb3JtYXR0ZXIgPSBuZXcgSW50bC5EYXRlVGltZUZvcm1hdChcImVuLVVTXCIsIHtcbiAgICB0aW1lWm9uZTogXCJBc2lhL1Nlb3VsXCIsXG4gICAgaG91cjogXCJudW1lcmljXCIsXG4gICAgaG91cjEyOiBmYWxzZSxcbiAgICB3ZWVrZGF5OiBcInNob3J0XCIsXG4gIH0pO1xuICBjb25zdCBwYXJ0cyA9IGZvcm1hdHRlci5mb3JtYXRUb1BhcnRzKGF0KTtcbiAgY29uc3QgaG91ciA9IE51bWJlcihwYXJ0cy5maW5kKChwKSA9PiBwLnR5cGUgPT09IFwiaG91clwiKT8udmFsdWUgPz8gXCIwXCIpO1xuICBjb25zdCB3ZWVrZGF5ID0gcGFydHMuZmluZCgocCkgPT4gcC50eXBlID09PSBcIndlZWtkYXlcIik/LnZhbHVlID8/IFwiXCI7XG4gIC8vIEludGwg7J2AIOyekOygleydhCAyNCDroZwg7KSEIOyImCDsnojri6QuIDAuLjIzIOycvOuhnCDrp57stpjri6QuXG4gIHJldHVybiB7IGhvdXI6IGhvdXIgJSAyNCwgd2Vla2RheSB9O1xufVxuXG4vKiog7KO866eQ7J206rGw64KYIDIw7IucfuydteydvCA47Iuc66m0IOyhsOyaqSDsi5zqsIQuICovXG5leHBvcnQgZnVuY3Rpb24gaXNRdWlldEhvdXJzKGF0OiBEYXRlID0gbmV3IERhdGUoKSk6IGJvb2xlYW4ge1xuICBjb25zdCB7IGhvdXIsIHdlZWtkYXkgfSA9IHNlb3VsUGFydHMoYXQpO1xuICBpZiAod2Vla2RheSA9PT0gXCJTYXRcIiB8fCB3ZWVrZGF5ID09PSBcIlN1blwiKSByZXR1cm4gdHJ1ZTtcbiAgcmV0dXJuIGhvdXIgPj0gUVVJRVRfRlJPTV9IT1VSIHx8IGhvdXIgPCBRVUlFVF9VTlRJTF9IT1VSO1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7QUFZQSxNQUFNLGtCQUFrQjs7QUFFeEIsTUFBTSxtQkFBbUI7Ozs7O0FBTXpCLFNBQVMsV0FBVyxJQUE2QztDQU8vRCxNQUFNLFFBQVEsSUFOUSxLQUFLLGVBQWUsU0FBUztFQUNqRCxVQUFVO0VBQ1YsTUFBTTtFQUNOLFFBQVE7RUFDUixTQUFTO0NBQ1gsQ0FDYyxDQUFBLENBQVUsY0FBYyxFQUFFO0NBQ3hDLE1BQU0sT0FBTyxPQUFPLE1BQU0sTUFBTSxNQUFNLEVBQUUsU0FBUyxNQUFNLENBQUMsRUFBRSxTQUFTLEdBQUc7Q0FDdEUsTUFBTSxVQUFVLE1BQU0sTUFBTSxNQUFNLEVBQUUsU0FBUyxTQUFTLENBQUMsRUFBRSxTQUFTO0NBRWxFLE9BQU87RUFBRSxNQUFNLE9BQU87RUFBSTtDQUFRO0FBQ3BDOztBQUdBLFNBQWdCLGFBQWEscUJBQVcsSUFBSSxLQUFLLEdBQVk7Q0FDM0QsTUFBTSxFQUFFLE1BQU0sWUFBWSxXQUFXLEVBQUU7Q0FDdkMsSUFBSSxZQUFZLFNBQVMsWUFBWSxPQUFPLE9BQU87Q0FDbkQsT0FBTyxRQUFRLG1CQUFtQixPQUFPO0FBQzNDIn0=
47
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVpZXQtaG91cnMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3F1aWV0LWhvdXJzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogU2xhY2sg7JWM66a8IOyhsOyaqSDsi5zqsIQuXG4gKlxuICog7Yag7YGwIOunjOujjOuKlCDsgqzrnozsnbQg7J6s66Gc6re47J247ZW07JW8IO2SgOumrOuKlOuNsCwg6re4IOyCrOuejOydtCDsnpDqsbDrgpgg7Ims64qUIOuPmeyViCDslYzroKTrj4Qg7ZWgIOyImCDsnojripRcbiAqIOydvOydtCDsl4bri6QuIOyXheustCDsi5zqsITsl5Drp4wg7JWM66as6rOgIOuCmOuouOyngOuKlCDri6TsnYwg6re866y0IOyLnOqwhOycvOuhnCDrr7jro6zri6Qg4oCUIOunjOujjCDslYzrprzsnYBcbiAqIOyjvOq4sOyggeycvOuhnCDrsJjrs7XrkJjrr4DroZwg7KGw7JqpIOyLnOqwhOyXkCDrsoTroKTrj4Qg7JWE7LmoIOyyqyDso7zquLDsl5Ag64uk7IucIOyYqOuLpC5cbiAqXG4gKiDri6Trp4wgcHJvdmlkZXIg7J2YIOuniOyngOuniSDthqDtgbDsnbQg7KO964qUIOqyg+ydgCDshJzruYTsiqQg7KCV7KeAIOyLoO2YuOudvCDsnbQg6rec7LmZ7JeQ7IScIOygnOyZuO2VnOuLpFxuICogKO2YuOy2nOu2gOqwgCBgdXJnZW50OiB0cnVlYCDroZwg67O064K066m0IOuQnOuLpCkuXG4gKi9cbmltcG9ydCB7IHR5cGUgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcblxuLyoqXG4gKiDshKTsoJUg7KGw7ZqMIO2VqOyImOydmCDtmJXtg5zrp4wg67mM66aw64ukIOKAlCBgdHlwZWAgaW1wb3J0IOudvCDrn7Dtg4DsnoQg7J2Y7KG07J20IOyDneq4sOyngCDslYrripTri6QuXG4gKlxuICog6rCSIOyekOyytOuKlCDtla3sg4Eg7Zi47Lac67aAKGBzbGFjay1ub3RpZnlgKeqwgCDrhJjquLTri6QuIOyXrOq4sOyEnCBgZ2V0U2V0dGluZ2Ag7J2EIOyLpOygnOuhnCBpbXBvcnQg7ZWY66m0XG4gKiBgc2V0dGluZy5zdG9yZSDihpIgc2V0dGluZy5tb2RlbCDihpIgdG9rZW4ubW9kZWxgIOydtCDrlLjroKTsmYAsIGB0b2tlbi5tb2RlbGAg7J2EIG1vY2sg7ZWY64qUXG4gKiDthYzsiqTtirjrk6TsnbQg7Iuk7KCcIOuqqOuTiOydhCDrqLzsoIAg66Gc65Oc7ZW0IOuyhOumsOuLpC4g7KGw7JqpIOyLnOqwhCDtjJDsoJXsnYAg7Iuc6rCBIOqzhOyCsOydtOyngCDshKTsoJUg7KGw7ZqM6rCAXG4gKiDslYTri4jrr4DroZwsIOydveuKlCDssYXsnoTsnYAg7J2066+4IOyEpOygleydhCDsnb3qs6Ag7J6I64qUIO2YuOy2nOu2gOyXkCDrkZTri6QuXG4gKi9cbnR5cGUgUmVhZFNldHRpbmcgPSB0eXBlb2YgZ2V0U2V0dGluZztcblxuLyoqIGBzZXR0aW5nLmNvbnN0YW50LnRzYCDsnZgg65GQIGZhbGxiYWNrIOqzvCDqsJnslYTslbwg7ZWc64ukLiDsnbzsuZgg7Jes67aA64qUIO2FjOyKpO2KuOqwgCDsp4Dtgqjri6QuICovXG5leHBvcnQgY29uc3QgREVGQVVMVF9RVUlFVF9GUk9NX0hPVVIgPSAyMDtcbmV4cG9ydCBjb25zdCBERUZBVUxUX1FVSUVUX1VOVElMX0hPVVIgPSA4O1xuXG4vKipcbiAqIOyEnOuyhOqwgCBVVEMg66GcIOuPjOyVhOuPhCDtlZzqta0g7Iuc6rCE7Jy866GcIO2MkOygle2VnOuLpC4gZGV2MCDripQgVVRDIOydtOuvgOuhnCBgZ2V0SG91cnMoKWAg66W8IOq3uOuMgOuhnFxuICog7JOw66m0IFwi7KCA64WBIDjsi5xcIuqwgCDtlZzqta0g7Iuc6rCEIOyDiOuyvSA17Iuc6rCAIOuQnOuLpC5cbiAqL1xuY29uc3QgU0VPVUxfRk9STUFUVEVSID0gbmV3IEludGwuRGF0ZVRpbWVGb3JtYXQoXCJlbi1VU1wiLCB7XG4gIHRpbWVab25lOiBcIkFzaWEvU2VvdWxcIixcbiAgaG91cjogXCJudW1lcmljXCIsXG4gIGhvdXIxMjogZmFsc2UsXG4gIHdlZWtkYXk6IFwic2hvcnRcIixcbn0pO1xuXG5mdW5jdGlvbiBzZW91bFBhcnRzKGF0OiBEYXRlKTogeyBob3VyOiBudW1iZXI7IHdlZWtkYXk6IHN0cmluZyB9IHtcbiAgY29uc3QgcGFydHMgPSBTRU9VTF9GT1JNQVRURVIuZm9ybWF0VG9QYXJ0cyhhdCk7XG4gIGNvbnN0IGhvdXIgPSBOdW1iZXIocGFydHMuZmluZCgocCkgPT4gcC50eXBlID09PSBcImhvdXJcIik/LnZhbHVlID8/IFwiMFwiKTtcbiAgY29uc3Qgd2Vla2RheSA9IHBhcnRzLmZpbmQoKHApID0+IHAudHlwZSA9PT0gXCJ3ZWVrZGF5XCIpPy52YWx1ZSA/PyBcIlwiO1xuICAvLyBJbnRsIOydgCDsnpDsoJXsnYQgMjQg66GcIOykhCDsiJgg7J6I64ukLiAwLi4yMyDsnLzroZwg66ee7LaY64ukLlxuICByZXR1cm4geyBob3VyOiBob3VyICUgMjQsIHdlZWtkYXkgfTtcbn1cblxuLyoqIDAuLjIzIOuylOychOulvCDrspfslrTrgpjqsbDrgpgg7Iir7J6Q6rCAIOyVhOuLiOuptCDquLDrs7jqsJLsnLzroZwg65ao7Ja07KeE64ukLiAqL1xuZnVuY3Rpb24gYm91bmRlZEhvdXIocmF3OiBzdHJpbmcgfCB1bmRlZmluZWQsIGZhbGxiYWNrOiBudW1iZXIpOiBudW1iZXIge1xuICBjb25zdCBwYXJzZWQgPSBOdW1iZXIocmF3KTtcbiAgaWYgKCFOdW1iZXIuaXNJbnRlZ2VyKHBhcnNlZCkgfHwgcGFyc2VkIDwgMCB8fCBwYXJzZWQgPiAyMykgcmV0dXJuIGZhbGxiYWNrO1xuICByZXR1cm4gcGFyc2VkO1xufVxuXG4vKipcbiAqIOyhsOyaqSDsi5zqsITsnbjsp4Ag7YyQ7KCV7ZWc64ukLlxuICpcbiAqIOyLnOyekSA+IOyiheujjOuptCDsnpDsoJXsnYQg64SY64qUIOq1rOqwhCgyMOyLnH7snbXsnbwgOOyLnCksIOyLnOyekSA8IOyiheujjOuptCDqsJnsnYAg64KgIOq1rOqwhCgw7Iucfjjsi5zsspjrn7wpXG4gKiDsnLzroZwg7J2964qU64ukLiDslbzqsIQg6re866y07LKY65+8IOuCruyXkCDsobDsmqntlZjqs6Ag7Iu27J2AIO2MgOuPhCDsnojslrQg65Kk7KeR7Z6MIOqwkuydhCDrp4nsp4Ag7JWK64qU64ukLlxuICovXG5leHBvcnQgZnVuY3Rpb24gaXNRdWlldEhvdXJzKGF0OiBEYXRlLCByZWFkU2V0dGluZzogUmVhZFNldHRpbmcpOiBib29sZWFuIHtcbiAgY29uc3QgeyBob3VyLCB3ZWVrZGF5IH0gPSBzZW91bFBhcnRzKGF0KTtcblxuICBjb25zdCBpc1dlZWtlbmQgPSB3ZWVrZGF5ID09PSBcIlNhdFwiIHx8IHdlZWtkYXkgPT09IFwiU3VuXCI7XG4gIGlmIChpc1dlZWtlbmQgJiYgcmVhZFNldHRpbmcoXCJzbGFjay5ub3RpZnlPbldlZWtlbmRzXCIsIFwiU0xBQ0tfTk9USUZZX09OX1dFRUtFTkRTXCIpICE9PSBcInRydWVcIikge1xuICAgIHJldHVybiB0cnVlO1xuICB9XG5cbiAgY29uc3QgZnJvbSA9IGJvdW5kZWRIb3VyKFxuICAgIHJlYWRTZXR0aW5nKFwic2xhY2sucXVpZXRGcm9tSG91clwiLCBcIlNMQUNLX1FVSUVUX0ZST01fSE9VUlwiKSxcbiAgICBERUZBVUxUX1FVSUVUX0ZST01fSE9VUixcbiAgKTtcbiAgY29uc3QgdW50aWwgPSBib3VuZGVkSG91cihcbiAgICByZWFkU2V0dGluZyhcInNsYWNrLnF1aWV0VW50aWxIb3VyXCIsIFwiU0xBQ0tfUVVJRVRfVU5USUxfSE9VUlwiKSxcbiAgICBERUZBVUxUX1FVSUVUX1VOVElMX0hPVVIsXG4gICk7XG5cbiAgLy8g7Iuc7J6R6rO8IOyiheujjOqwgCDqsJnsnLzrqbQg6rWs6rCE7J20IOu5hOyWtCDsnojri6TripQg65y77Jy866GcIOydveyWtCDsobDsmqkg7Iuc6rCE7J2EIOuRkOyngCDslYrripTri6QuXG4gIGlmIChmcm9tID09PSB1bnRpbCkgcmV0dXJuIGZhbHNlO1xuICByZXR1cm4gZnJvbSA+IHVudGlsID8gaG91ciA+PSBmcm9tIHx8IGhvdXIgPCB1bnRpbCA6IGhvdXIgPj0gZnJvbSAmJiBob3VyIDwgdW50aWw7XG59XG4iXSwibWFwcGluZ3MiOiI7O0FBdUJBLE1BQWEsMEJBQTBCO0FBQ3ZDLE1BQWEsMkJBQTJCOzs7OztBQU14QyxNQUFNLGtCQUFrQixJQUFJLEtBQUssZUFBZSxTQUFTO0NBQ3ZELFVBQVU7Q0FDVixNQUFNO0NBQ04sUUFBUTtDQUNSLFNBQVM7QUFDWCxDQUFDO0FBRUQsU0FBUyxXQUFXLElBQTZDO0NBQy9ELE1BQU0sUUFBUSxnQkFBZ0IsY0FBYyxFQUFFO0NBQzlDLE1BQU0sT0FBTyxPQUFPLE1BQU0sTUFBTSxNQUFNLEVBQUUsU0FBUyxNQUFNLENBQUMsRUFBRSxTQUFTLEdBQUc7Q0FDdEUsTUFBTSxVQUFVLE1BQU0sTUFBTSxNQUFNLEVBQUUsU0FBUyxTQUFTLENBQUMsRUFBRSxTQUFTO0NBRWxFLE9BQU87RUFBRSxNQUFNLE9BQU87RUFBSTtDQUFRO0FBQ3BDOztBQUdBLFNBQVMsWUFBWSxLQUF5QixVQUEwQjtDQUN0RSxNQUFNLFNBQVMsT0FBTyxHQUFHO0NBQ3pCLElBQUksQ0FBQyxPQUFPLFVBQVUsTUFBTSxLQUFLLFNBQVMsS0FBSyxTQUFTLElBQUksT0FBTztDQUNuRSxPQUFPO0FBQ1Q7Ozs7Ozs7QUFRQSxTQUFnQixhQUFhLElBQVUsYUFBbUM7Q0FDeEUsTUFBTSxFQUFFLE1BQU0sWUFBWSxXQUFXLEVBQUU7Q0FHdkMsS0FEa0IsWUFBWSxTQUFTLFlBQVksVUFDbEMsWUFBWSwwQkFBMEIsMEJBQTBCLE1BQU0sUUFDckYsT0FBTztDQUdULE1BQU0sT0FBTyxZQUNYLFlBQVksdUJBQXVCLHVCQUF1QixHQUFBLEVBRTVEO0NBQ0EsTUFBTSxRQUFRLFlBQ1osWUFBWSx3QkFBd0Isd0JBQXdCLEdBQUEsQ0FFOUQ7Q0FHQSxJQUFJLFNBQVMsT0FBTyxPQUFPO0NBQzNCLE9BQU8sT0FBTyxRQUFRLFFBQVEsUUFBUSxPQUFPLFFBQVEsUUFBUSxRQUFRLE9BQU87QUFDOUUifQ==
@@ -25,7 +25,11 @@ async function notifySlack(notification, readSetting = getSetting) {
25
25
  logger.debug(`slack not configured, skipping notification: ${title} ${subject ?? ""}`);
26
26
  return;
27
27
  }
28
- if (!urgent && isQuietHours(now)) {
28
+ if (!urgent && readSetting("slack.enabled", "SLACK_ENABLED") === "false") {
29
+ logger.debug(`slack disabled, skipping notification: ${title} ${subject ?? ""}`);
30
+ return;
31
+ }
32
+ if (!urgent && isQuietHours(now ?? /* @__PURE__ */ new Date(), readSetting)) {
29
33
  logger.debug(`quiet hours, skipping notification: ${title} ${subject ?? ""}`);
30
34
  return;
31
35
  }
@@ -69,4 +73,4 @@ async function notifySlack(notification, readSetting = getSetting) {
69
73
  //#endregion
70
74
  export { SLACK_COLOR, notifySlack };
71
75
 
72
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyhsOyaqSDsi5zqsIQo7KO866eQwrcyMH447IucKeyXkOuKlCBgdXJnZW50YCDqsIAg7JWE64uMIOyVjOumvOydhCDrs7TrgrTsp4Ag7JWK64qU64ukLlxuICog7JWM66a8IOqyveuhnOqwgCDthqDtgbAg65287Jqw7YyF7J2064KYIOyYpOulmCDsoITtjIzrpbwg7KeA7JewwrfssKjri6jtlbTshJzripQg7JWIIOuQnOuLpC5cbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcbmltcG9ydCB7IGlzUXVpZXRIb3VycyB9IGZyb20gXCIuL3F1aWV0LWhvdXJzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG4gIC8qKlxuICAgKiDsobDsmqkg7Iuc6rCEKOyjvOunkMK3MjB+OOyLnCnsl5Drj4Qg67O064K464ukLiDshJzruYTsiqTqsIAg66mI7LaYIOyDge2DnOyymOufvCDsp4DquIgg7JWM7KeAIOyViuycvOuptCDshpDtlbTqsIBcbiAgICog7Luk7KeA64qUIOyCrOqxtOyXkOunjCDsk7Tri6Qg4oCUIOuCqOyaqe2VmOuptCDsobDsmqkg7Iuc6rCEIOyekOyytOqwgCDrrLTsnZjrr7jtlbTsp4Tri6QuXG4gICAqL1xuICB1cmdlbnQ/OiBib29sZWFuO1xuICAvKiog7KGw7JqpIOyLnOqwhCDtjJDsoJUg6riw7KSAIOyLnOqwgS4g7YWM7Iqk7Yq47JeQ7IScIOyLnOqzhOulvCDqs6DsoJXtlaAg65WM66eMIOuEmOq4tOuLpC4gKi9cbiAgbm93PzogRGF0ZTtcbn07XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBub3RpZnlTbGFjayhcbiAgbm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbixcbiAgcmVhZFNldHRpbmc6IHR5cGVvZiBnZXRTZXR0aW5nID0gZ2V0U2V0dGluZyxcbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHJlYWRTZXR0aW5nKFwic2xhY2suYm90VG9rZW5cIiwgXCJTTEFDS19CT1RfVE9LRU5cIik7XG4gIGNvbnN0IGNoYW5uZWwgPSByZWFkU2V0dGluZyhcInNsYWNrLmNoYW5uZWxJZFwiLCBcIlNMQUNLX0NIQU5ORUxfSURcIik7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yLCB1cmdlbnQsIG5vdyB9ID0gbm90aWZpY2F0aW9uO1xuICBpZiAoIWJvdFRva2VuIHx8ICFjaGFubmVsKSB7XG4gICAgbG9nZ2VyLmRlYnVnKGBzbGFjayBub3QgY29uZmlndXJlZCwgc2tpcHBpbmcgbm90aWZpY2F0aW9uOiAke3RpdGxlfSAke3N1YmplY3QgPz8gXCJcIn1gKTtcbiAgICByZXR1cm47XG4gIH1cblxuICAvLyDrp4zro4wg7JWM66a87J2AIOyjvOq4sOyggeycvOuhnCDrsJjrs7XrkJjrr4DroZwg7Jes6riw7IScIOuyhOugpOuPhCDri6TsnYwg6re866y0IOyLnOqwhCDssqsg7KO86riw7JeQIOuLpOyLnCDsmKjri6QuXG4gIGlmICghdXJnZW50ICYmIGlzUXVpZXRIb3Vycyhub3cpKSB7XG4gICAgbG9nZ2VyLmRlYnVnKGBxdWlldCBob3Vycywgc2tpcHBpbmcgbm90aWZpY2F0aW9uOiAke3RpdGxlfSAke3N1YmplY3QgPz8gXCJcIn1gKTtcbiAgICByZXR1cm47XG4gIH1cblxuICBjb25zdCBibG9ja3M6IHVua25vd25bXSA9IFtcbiAgICB7XG4gICAgICB0eXBlOiBcInNlY3Rpb25cIixcbiAgICAgIHRleHQ6IHsgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogc3ViamVjdCA/IGAqJHt0aXRsZX0qICBcXGAke3N1YmplY3R9XFxgYCA6IGAqJHt0aXRsZX0qYCB9LFxuICAgIH0sXG4gIF07XG4gIGlmIChjb250ZXh0KSB7XG4gICAgYmxvY2tzLnB1c2goeyB0eXBlOiBcImNvbnRleHRcIiwgZWxlbWVudHM6IFt7IHR5cGU6IFwibXJrZHduXCIsIHRleHQ6IGNvbnRleHQgfV0gfSk7XG4gIH1cblxuICB0cnkge1xuICAgIGNvbnN0IHJlcyA9IGF3YWl0IGZldGNoKFNMQUNLX1BPU1RfTUVTU0FHRV9VUkwsIHtcbiAgICAgIG1ldGhvZDogXCJQT1NUXCIsXG4gICAgICBoZWFkZXJzOiB7XG4gICAgICAgIFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOFwiLFxuICAgICAgICBBdXRob3JpemF0aW9uOiBgQmVhcmVyICR7Ym90VG9rZW59YCxcbiAgICAgIH0sXG4gICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICAgIGNoYW5uZWwsXG4gICAgICAgIC8vIGZhbGxiYWNrIOydgCBhdHRhY2htZW50IOyViOyXkCDrkZTri6QuIOy1nOyDgeychCB0ZXh0IOuhnCDrs7TrgrTrqbQgYmxvY2tzIOqwgCDsoJXsg4Eg66CM642U65CY64qUXG4gICAgICAgIC8vIO2ZlOuptOyXkOyEnOuPhCDtlajqu5gg7Lac66Cl64+8IOygnOuqqeydtCDrkZAg67KIIOuztOyduOuLpC4gYXR0YWNobWVudCBmYWxsYmFjayDsnYAgYmxvY2tzIOulvFxuICAgICAgICAvLyDrqrsg6re466as64qUIO2BtOudvOydtOyWuO2KuOyZgCDtkbjsi5wg7JWM66a87JeQ7ISc66eMIOyTsOyduOuLpC5cbiAgICAgICAgYXR0YWNobWVudHM6IFtcbiAgICAgICAgICB7XG4gICAgICAgICAgICBmYWxsYmFjazogc3ViamVjdCA/IGAke3RpdGxlfSDigJQgJHtzdWJqZWN0fWAgOiB0aXRsZSxcbiAgICAgICAgICAgIC4uLihjb2xvciA/IHsgY29sb3IgfSA6IHt9KSxcbiAgICAgICAgICAgIGJsb2NrcyxcbiAgICAgICAgICB9LFxuICAgICAgICBdLFxuICAgICAgfSksXG4gICAgICBzaWduYWw6IEFib3J0U2lnbmFsLnRpbWVvdXQoU0xBQ0tfVElNRU9VVF9NUyksXG4gICAgfSk7XG5cbiAgICAvLyBib3Qg66+47LSI64yAKG5vdF9pbl9jaGFubmVsKcK37J6Y66q765CcIOyxhOuEkCDrk7Eg6rCA7J6lIO2dlO2VnCDsmKTshKTsoJXsnYAgSFRUUCAyMDAgKyBvazpmYWxzZSDroZwg7Jio64ukLlxuICAgIC8vIHN0YXR1cyDrp4wg67O066m0IOyhsOyaqe2eiCDshLHqs7Ug7LKY66as64+8IOyVjOumvOydtCDthrXsp7jroZwg7IKs65287KeE64ukLlxuICAgIGNvbnN0IGJvZHkgPSAoYXdhaXQgcmVzLmpzb24oKS5jYXRjaCgoKSA9PiBudWxsKSkgYXMgeyBvaz86IGJvb2xlYW47IGVycm9yPzogc3RyaW5nIH0gfCBudWxsO1xuICAgIGlmICghYm9keT8ub2spIHtcbiAgICAgIGxvZ2dlci53YXJuKGBzbGFjayBub3RpZmljYXRpb24gcmVqZWN0ZWQ6ICR7Ym9keT8uZXJyb3IgPz8gYGh0dHAgJHtyZXMuc3RhdHVzfWB9YCk7XG4gICAgfVxuICB9IGNhdGNoIChlKSB7XG4gICAgbG9nZ2VyLndhcm4oYHNsYWNrIG5vdGlmaWNhdGlvbiBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCk7XG4gIH1cbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7QUFZQSxNQUFNLFNBQVMsVUFBVSxDQUFDLFNBQVMsT0FBTyxDQUFDO0FBRTNDLE1BQU0seUJBQXlCO0FBQy9CLE1BQU0sbUJBQW1COztBQUd6QixNQUFhLGNBQWM7Q0FDekIsTUFBTTtDQUNOLEtBQUs7QUFDUDtBQW1CQSxlQUFzQixZQUNwQixjQUNBLGNBQWlDLFlBQ2xCO0NBQ2YsTUFBTSxXQUFXLFlBQVksa0JBQWtCLGlCQUFpQjtDQUNoRSxNQUFNLFVBQVUsWUFBWSxtQkFBbUIsa0JBQWtCO0NBQ2pFLE1BQU0sRUFBRSxPQUFPLFNBQVMsU0FBUyxPQUFPLFFBQVEsUUFBUTtDQUN4RCxJQUFJLENBQUMsWUFBWSxDQUFDLFNBQVM7RUFDekIsT0FBTyxNQUFNLGdEQUFnRCxNQUFNLEdBQUcsV0FBVyxJQUFJO0VBQ3JGO0NBQ0Y7Q0FHQSxJQUFJLENBQUMsVUFBVSxhQUFhLEdBQUcsR0FBRztFQUNoQyxPQUFPLE1BQU0sdUNBQXVDLE1BQU0sR0FBRyxXQUFXLElBQUk7RUFDNUU7Q0FDRjtDQUVBLE1BQU0sU0FBb0IsQ0FDeEI7RUFDRSxNQUFNO0VBQ04sTUFBTTtHQUFFLE1BQU07R0FBVSxNQUFNLFVBQVUsSUFBSSxNQUFNLE9BQU8sUUFBUSxNQUFNLElBQUksTUFBTTtFQUFHO0NBQ3RGLENBQ0Y7Q0FDQSxJQUFJLFNBQ0YsT0FBTyxLQUFLO0VBQUUsTUFBTTtFQUFXLFVBQVUsQ0FBQztHQUFFLE1BQU07R0FBVSxNQUFNO0VBQVEsQ0FBQztDQUFFLENBQUM7Q0FHaEYsSUFBSTtFQUNGLE1BQU0sTUFBTSxNQUFNLE1BQU0sd0JBQXdCO0dBQzlDLFFBQVE7R0FDUixTQUFTO0lBQ1AsZ0JBQWdCO0lBQ2hCLGVBQWUsVUFBVTtHQUMzQjtHQUNBLE1BQU0sS0FBSyxVQUFVO0lBQ25CO0lBSUEsYUFBYSxDQUNYO0tBQ0UsVUFBVSxVQUFVLEdBQUcsTUFBTSxLQUFLLFlBQVk7S0FDOUMsR0FBSSxRQUFRLEVBQUUsTUFBTSxJQUFJLENBQUM7S0FDekI7SUFDRixDQUNGO0dBQ0YsQ0FBQztHQUNELFFBQVEsWUFBWSxRQUFRLGdCQUFnQjtFQUM5QyxDQUFDO0VBSUQsTUFBTSxPQUFRLE1BQU0sSUFBSSxLQUFLLENBQUMsQ0FBQyxZQUFZLElBQUk7RUFDL0MsSUFBSSxDQUFDLE1BQU0sSUFDVCxPQUFPLEtBQUssZ0NBQWdDLE1BQU0sU0FBUyxRQUFRLElBQUksVUFBVTtDQUVyRixTQUFTLEdBQUc7RUFDVixPQUFPLEtBQUssOEJBQStCLEVBQVksU0FBUztDQUNsRTtBQUNGIn0=
76
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyhsOyaqSDsi5zqsIQo7KO866eQwrcyMH447IucKeyXkOuKlCBgdXJnZW50YCDqsIAg7JWE64uMIOyVjOumvOydhCDrs7TrgrTsp4Ag7JWK64qU64ukLlxuICog7JWM66a8IOqyveuhnOqwgCDthqDtgbAg65287Jqw7YyF7J2064KYIOyYpOulmCDsoITtjIzrpbwg7KeA7JewwrfssKjri6jtlbTshJzripQg7JWIIOuQnOuLpC5cbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcbmltcG9ydCB7IGlzUXVpZXRIb3VycyB9IGZyb20gXCIuL3F1aWV0LWhvdXJzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG4gIC8qKlxuICAgKiDsobDsmqkg7Iuc6rCEKOyjvOunkMK3MjB+OOyLnCnsl5Drj4Qg67O064K464ukLiDshJzruYTsiqTqsIAg66mI7LaYIOyDge2DnOyymOufvCDsp4DquIgg7JWM7KeAIOyViuycvOuptCDshpDtlbTqsIBcbiAgICog7Luk7KeA64qUIOyCrOqxtOyXkOunjCDsk7Tri6Qg4oCUIOuCqOyaqe2VmOuptCDsobDsmqkg7Iuc6rCEIOyekOyytOqwgCDrrLTsnZjrr7jtlbTsp4Tri6QuXG4gICAqL1xuICB1cmdlbnQ/OiBib29sZWFuO1xuICAvKiog7KGw7JqpIOyLnOqwhCDtjJDsoJUg6riw7KSAIOyLnOqwgS4g7YWM7Iqk7Yq47JeQ7IScIOyLnOqzhOulvCDqs6DsoJXtlaAg65WM66eMIOuEmOq4tOuLpC4gKi9cbiAgbm93PzogRGF0ZTtcbn07XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBub3RpZnlTbGFjayhcbiAgbm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbixcbiAgcmVhZFNldHRpbmc6IHR5cGVvZiBnZXRTZXR0aW5nID0gZ2V0U2V0dGluZyxcbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHJlYWRTZXR0aW5nKFwic2xhY2suYm90VG9rZW5cIiwgXCJTTEFDS19CT1RfVE9LRU5cIik7XG4gIGNvbnN0IGNoYW5uZWwgPSByZWFkU2V0dGluZyhcInNsYWNrLmNoYW5uZWxJZFwiLCBcIlNMQUNLX0NIQU5ORUxfSURcIik7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yLCB1cmdlbnQsIG5vdyB9ID0gbm90aWZpY2F0aW9uO1xuICBpZiAoIWJvdFRva2VuIHx8ICFjaGFubmVsKSB7XG4gICAgbG9nZ2VyLmRlYnVnKGBzbGFjayBub3QgY29uZmlndXJlZCwgc2tpcHBpbmcgbm90aWZpY2F0aW9uOiAke3RpdGxlfSAke3N1YmplY3QgPz8gXCJcIn1gKTtcbiAgICByZXR1cm47XG4gIH1cblxuICAvLyDsl7DtnLTsspjrn7wg6rec7LmZ7Jy866GcIOyeoeydhCDsiJgg7JeG64qUIOq4sOqwhOyXkCDqtIDrpqzsnpDqsIAg64K066as64qUIOyKpOychOy5mC4gdXJnZW50IOuKlCDthrXqs7zsi5ztgqjri6Qg4oCUXG4gIC8vIOuBiCDsg4Htg5zroZwgcHJvdmlkZXIg6rCAIOyghOu2gCDso73snLzrqbQg7Jew7Zy0IOuCtOuCtCDshJzruYTsiqTqsIAg66mI7LaYIOykhCDslYTrrLTrj4Qg66qo66W464ukLlxuICBpZiAoIXVyZ2VudCAmJiByZWFkU2V0dGluZyhcInNsYWNrLmVuYWJsZWRcIiwgXCJTTEFDS19FTkFCTEVEXCIpID09PSBcImZhbHNlXCIpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIGRpc2FibGVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIC8vIOunjOujjCDslYzrprzsnYAg7KO86riw7KCB7Jy866GcIOuwmOuzteuQmOuvgOuhnCDsl6zquLDshJwg67KE66Ck64+EIOuLpOydjCDqt7zrrLQg7Iuc6rCEIOyyqyDso7zquLDsl5Ag64uk7IucIOyYqOuLpC5cbiAgaWYgKCF1cmdlbnQgJiYgaXNRdWlldEhvdXJzKG5vdyA/PyBuZXcgRGF0ZSgpLCByZWFkU2V0dGluZykpIHtcbiAgICBsb2dnZXIuZGVidWcoYHF1aWV0IGhvdXJzLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGJsb2NrczogdW5rbm93bltdID0gW1xuICAgIHtcbiAgICAgIHR5cGU6IFwic2VjdGlvblwiLFxuICAgICAgdGV4dDogeyB0eXBlOiBcIm1ya2R3blwiLCB0ZXh0OiBzdWJqZWN0ID8gYCoke3RpdGxlfSogIFxcYCR7c3ViamVjdH1cXGBgIDogYCoke3RpdGxlfSpgIH0sXG4gICAgfSxcbiAgXTtcbiAgaWYgKGNvbnRleHQpIHtcbiAgICBibG9ja3MucHVzaCh7IHR5cGU6IFwiY29udGV4dFwiLCBlbGVtZW50czogW3sgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogY29udGV4dCB9XSB9KTtcbiAgfVxuXG4gIHRyeSB7XG4gICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PXV0Zi04XCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHtib3RUb2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHtcbiAgICAgICAgY2hhbm5lbCxcbiAgICAgICAgLy8gZmFsbGJhY2sg7J2AIGF0dGFjaG1lbnQg7JWI7JeQIOuRlOuLpC4g7LWc7IOB7JyEIHRleHQg66GcIOuztOuCtOuptCBibG9ja3Mg6rCAIOygleyDgSDroIzrjZTrkJjripRcbiAgICAgICAgLy8g7ZmU66m07JeQ7ISc64+EIO2VqOq7mCDstpzroKXrj7wg7KCc66qp7J20IOuRkCDrsogg67O07J2464ukLiBhdHRhY2htZW50IGZhbGxiYWNrIOydgCBibG9ja3Mg66W8XG4gICAgICAgIC8vIOuquyDqt7jrpqzripQg7YG065287J207Ja47Yq47JmAIO2RuOyLnCDslYzrprzsl5DshJzrp4wg7JOw7J2464ukLlxuICAgICAgICBhdHRhY2htZW50czogW1xuICAgICAgICAgIHtcbiAgICAgICAgICAgIGZhbGxiYWNrOiBzdWJqZWN0ID8gYCR7dGl0bGV9IOKAlCAke3N1YmplY3R9YCA6IHRpdGxlLFxuICAgICAgICAgICAgLi4uKGNvbG9yID8geyBjb2xvciB9IDoge30pLFxuICAgICAgICAgICAgYmxvY2tzLFxuICAgICAgICAgIH0sXG4gICAgICAgIF0sXG4gICAgICB9KSxcbiAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dChTTEFDS19USU1FT1VUX01TKSxcbiAgICB9KTtcblxuICAgIC8vIGJvdCDrr7jstIjrjIAobm90X2luX2NoYW5uZWwpwrfsnpjrqrvrkJwg7LGE64SQIOuTsSDqsIDsnqUg7Z2U7ZWcIOyYpOyEpOygleydgCBIVFRQIDIwMCArIG9rOmZhbHNlIOuhnCDsmKjri6QuXG4gICAgLy8gc3RhdHVzIOunjCDrs7TrqbQg7KGw7Jqp7Z6IIOyEseqztSDsspjrpqzrj7wg7JWM66a87J20IO2GteynuOuhnCDsgqzrnbzsp4Tri6QuXG4gICAgY29uc3QgYm9keSA9IChhd2FpdCByZXMuanNvbigpLmNhdGNoKCgpID0+IG51bGwpKSBhcyB7IG9rPzogYm9vbGVhbjsgZXJyb3I/OiBzdHJpbmcgfSB8IG51bGw7XG4gICAgaWYgKCFib2R5Py5vaykge1xuICAgICAgbG9nZ2VyLndhcm4oYHNsYWNrIG5vdGlmaWNhdGlvbiByZWplY3RlZDogJHtib2R5Py5lcnJvciA/PyBgaHR0cCAke3Jlcy5zdGF0dXN9YH1gKTtcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dnZXIud2Fybihgc2xhY2sgbm90aWZpY2F0aW9uIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7OztBQVlBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxPQUFPLENBQUM7QUFFM0MsTUFBTSx5QkFBeUI7QUFDL0IsTUFBTSxtQkFBbUI7O0FBR3pCLE1BQWEsY0FBYztDQUN6QixNQUFNO0NBQ04sS0FBSztBQUNQO0FBbUJBLGVBQXNCLFlBQ3BCLGNBQ0EsY0FBaUMsWUFDbEI7Q0FDZixNQUFNLFdBQVcsWUFBWSxrQkFBa0IsaUJBQWlCO0NBQ2hFLE1BQU0sVUFBVSxZQUFZLG1CQUFtQixrQkFBa0I7Q0FDakUsTUFBTSxFQUFFLE9BQU8sU0FBUyxTQUFTLE9BQU8sUUFBUSxRQUFRO0NBQ3hELElBQUksQ0FBQyxZQUFZLENBQUMsU0FBUztFQUN6QixPQUFPLE1BQU0sZ0RBQWdELE1BQU0sR0FBRyxXQUFXLElBQUk7RUFDckY7Q0FDRjtDQUlBLElBQUksQ0FBQyxVQUFVLFlBQVksaUJBQWlCLGVBQWUsTUFBTSxTQUFTO0VBQ3hFLE9BQU8sTUFBTSwwQ0FBMEMsTUFBTSxHQUFHLFdBQVcsSUFBSTtFQUMvRTtDQUNGO0NBR0EsSUFBSSxDQUFDLFVBQVUsYUFBYSx1QkFBTyxJQUFJLEtBQUssR0FBRyxXQUFXLEdBQUc7RUFDM0QsT0FBTyxNQUFNLHVDQUF1QyxNQUFNLEdBQUcsV0FBVyxJQUFJO0VBQzVFO0NBQ0Y7Q0FFQSxNQUFNLFNBQW9CLENBQ3hCO0VBQ0UsTUFBTTtFQUNOLE1BQU07R0FBRSxNQUFNO0dBQVUsTUFBTSxVQUFVLElBQUksTUFBTSxPQUFPLFFBQVEsTUFBTSxJQUFJLE1BQU07RUFBRztDQUN0RixDQUNGO0NBQ0EsSUFBSSxTQUNGLE9BQU8sS0FBSztFQUFFLE1BQU07RUFBVyxVQUFVLENBQUM7R0FBRSxNQUFNO0dBQVUsTUFBTTtFQUFRLENBQUM7Q0FBRSxDQUFDO0NBR2hGLElBQUk7RUFDRixNQUFNLE1BQU0sTUFBTSxNQUFNLHdCQUF3QjtHQUM5QyxRQUFRO0dBQ1IsU0FBUztJQUNQLGdCQUFnQjtJQUNoQixlQUFlLFVBQVU7R0FDM0I7R0FDQSxNQUFNLEtBQUssVUFBVTtJQUNuQjtJQUlBLGFBQWEsQ0FDWDtLQUNFLFVBQVUsVUFBVSxHQUFHLE1BQU0sS0FBSyxZQUFZO0tBQzlDLEdBQUksUUFBUSxFQUFFLE1BQU0sSUFBSSxDQUFDO0tBQ3pCO0lBQ0YsQ0FDRjtHQUNGLENBQUM7R0FDRCxRQUFRLFlBQVksUUFBUSxnQkFBZ0I7RUFDOUMsQ0FBQztFQUlELE1BQU0sT0FBUSxNQUFNLElBQUksS0FBSyxDQUFDLENBQUMsWUFBWSxJQUFJO0VBQy9DLElBQUksQ0FBQyxNQUFNLElBQ1QsT0FBTyxLQUFLLGdDQUFnQyxNQUFNLFNBQVMsUUFBUSxJQUFJLFVBQVU7Q0FFckYsU0FBUyxHQUFHO0VBQ1YsT0FBTyxLQUFLLDhCQUErQixFQUFZLFNBQVM7Q0FDbEU7QUFDRiJ9
@@ -10,6 +10,7 @@ import {
10
10
  buildReminderContext,
11
11
  type ExpiredTokenReminderDeps,
12
12
  rescheduleExpiredTokenReminder,
13
+ sendExpiredTokenReminderNow,
13
14
  startExpiredTokenReminder,
14
15
  stopExpiredTokenReminder,
15
16
  } from "./expired-token-reminder";
@@ -88,12 +89,14 @@ describe("buildReminderContext", () => {
88
89
  describe("expired token reminder scheduling", () => {
89
90
  let minutes = "10";
90
91
  let rawUserMap = "yds:U-OLD";
92
+ let remindersEnabled = "true";
91
93
  let deps: ExpiredTokenReminderDeps;
92
94
 
93
95
  beforeEach(() => {
94
96
  vi.useFakeTimers();
95
97
  minutes = "10";
96
98
  rawUserMap = "yds:U-OLD";
99
+ remindersEnabled = "true";
97
100
  findInactiveMock.mockReset();
98
101
  getSettingMock.mockReset();
99
102
  notifySlackMock.mockReset();
@@ -101,6 +104,7 @@ describe("expired token reminder scheduling", () => {
101
104
  getSettingMock.mockImplementation((key: string) => {
102
105
  if (key === "slack.expiryReminderMinutes") return minutes;
103
106
  if (key === "slack.userMap") return rawUserMap;
107
+ if (key === "slack.remindersEnabled") return remindersEnabled;
104
108
  return undefined;
105
109
  });
106
110
  notifySlackMock.mockResolvedValue(undefined);
@@ -124,6 +128,45 @@ describe("expired token reminder scheduling", () => {
124
128
  expect(notifySlackMock).toHaveBeenCalledTimes(1);
125
129
  });
126
130
 
131
+ it("반복을 끄면 부팅해도 알리지 않는다", async () => {
132
+ remindersEnabled = "false";
133
+
134
+ startExpiredTokenReminder(deps);
135
+ await vi.advanceTimersByTimeAsync(0);
136
+ await vi.advanceTimersByTimeAsync(10 * 60_000);
137
+
138
+ expect(notifySlackMock).not.toHaveBeenCalled();
139
+ });
140
+
141
+ it("반복을 꺼도 주기 값은 남아 다시 켜면 그대로 쓴다", async () => {
142
+ remindersEnabled = "false";
143
+ rescheduleExpiredTokenReminder(deps);
144
+
145
+ remindersEnabled = "true";
146
+ rescheduleExpiredTokenReminder(deps);
147
+ await vi.advanceTimersByTimeAsync(10 * 60_000);
148
+
149
+ // 저장된 10분 주기가 그대로 살아 있다.
150
+ expect(notifySlackMock).toHaveBeenCalledTimes(1);
151
+ });
152
+
153
+ it("수동 트리거는 조용 시간을 통과하도록 urgent 로 보낸다", async () => {
154
+ // 사람이 직접 누른 것이라 밤이라고 삼키면 버튼이 고장 난 것으로 읽힌다.
155
+ const sent = await sendExpiredTokenReminderNow(deps);
156
+
157
+ expect(sent).toBe(1);
158
+ expect(notifySlackMock).toHaveBeenCalledWith(expect.objectContaining({ urgent: true }));
159
+ });
160
+
161
+ it("보낼 대상이 없으면 0을 돌려주고 아무것도 보내지 않는다", async () => {
162
+ findInactiveMock.mockResolvedValue([]);
163
+
164
+ const sent = await sendExpiredTokenReminderNow(deps);
165
+
166
+ expect(sent).toBe(0);
167
+ expect(notifySlackMock).not.toHaveBeenCalled();
168
+ });
169
+
127
170
  it("start를 다시 호출해도 interval은 하나만 남는다", async () => {
128
171
  startExpiredTokenReminder(deps);
129
172
  startExpiredTokenReminder(deps);
@@ -53,9 +53,9 @@ export function buildReminderContext(
53
53
  return [...lines, "재로그인이 필요합니다"].filter(Boolean).join("\n");
54
54
  }
55
55
 
56
- async function sendReminder(deps: ExpiredTokenReminderDeps): Promise<void> {
56
+ async function sendReminder(deps: ExpiredTokenReminderDeps, urgent = false): Promise<number> {
57
57
  const inactive = await deps.findInactive();
58
- if (inactive.length === 0) return;
58
+ if (inactive.length === 0) return 0;
59
59
 
60
60
  const userMap = deps.getSlackUserMap();
61
61
 
@@ -65,7 +65,21 @@ async function sendReminder(deps: ExpiredTokenReminderDeps): Promise<void> {
65
65
  subject: inactive.length > 1 ? `${inactive.length}건` : (inactive[0]!.name ?? "unnamed"),
66
66
  context: buildReminderContext(inactive, userMap),
67
67
  color: SLACK_COLOR.bad,
68
+ urgent,
68
69
  });
70
+ return inactive.length;
71
+ }
72
+
73
+ /**
74
+ * 화면의 "지금 보내기". 사람이 직접 누른 것이므로 조용 시간·마스터 스위치를 통과시킨다 —
75
+ * 눌렀는데 아무 일도 안 일어나면 버튼이 고장 난 것으로 읽힌다.
76
+ *
77
+ * 보낸 건수를 돌려줘 호출부가 "보낼 대상 없음"과 "보냄"을 구분해 보여준다.
78
+ */
79
+ export function sendExpiredTokenReminderNow(
80
+ deps: ExpiredTokenReminderDeps = defaultDeps,
81
+ ): Promise<number> {
82
+ return sendReminder(deps, true);
69
83
  }
70
84
 
71
85
  let timer: ReturnType<typeof setInterval> | null = null;
@@ -73,6 +87,12 @@ let timer: ReturnType<typeof setInterval> | null = null;
73
87
  function scheduleExpiredTokenReminder(runNow: boolean, deps: ExpiredTokenReminderDeps): void {
74
88
  stopExpiredTokenReminder();
75
89
 
90
+ // 주기 값과 별개인 스위치다. 꺼도 주기는 남아 있어 다시 켤 때 고르던 값이 그대로다.
91
+ if (deps.readSetting("slack.remindersEnabled", "SLACK_REMINDERS_ENABLED") === "false") {
92
+ logger.info("expired token reminder disabled");
93
+ return;
94
+ }
95
+
76
96
  const minutes = Number(
77
97
  deps.readSetting("slack.expiryReminderMinutes", "SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES") ?? 0,
78
98
  );
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.schema";
3
+ import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.constant";
4
4
 
5
5
  describe("validateSettingValue", () => {
6
6
  const intDef = findSettingDef("openai.maxWorkersPerToken")!;
@@ -37,6 +37,35 @@ describe("validateSettingValue", () => {
37
37
  });
38
38
  });
39
39
 
40
+ describe("preset 검증", () => {
41
+ const presetDef = findSettingDef("slack.expiryReminderMinutes")!;
42
+
43
+ it("목록에 있는 값만 받는다", () => {
44
+ expect(validateSettingValue(presetDef, "30")).toMatchObject({ ok: true, value: "30" });
45
+ expect(validateSettingValue(presetDef, "180")).toMatchObject({ ok: true });
46
+ });
47
+
48
+ it("목록 밖의 값은 거부한다", () => {
49
+ // 저장되면 화면에서 아무 버튼도 선택돼 보이지 않아 "왜 안 바뀌지"가 된다.
50
+ expect(validateSettingValue(presetDef, "45")).toMatchObject({ ok: false });
51
+ expect(validateSettingValue(presetDef, "0")).toMatchObject({ ok: false });
52
+ expect(validateSettingValue(presetDef, "많이")).toMatchObject({ ok: false });
53
+ });
54
+ });
55
+
56
+ describe("조용 시간 설정", () => {
57
+ // fallback("20"/"8")은 quiet-hours.ts 의 DEFAULT_QUIET_* 와 같아야 하지만 테스트로 묶지
58
+ // 않는다 — 두 모듈을 한 파일에서 import 하면 setting.store → token.model 이 딸려와,
59
+ // token.model 을 mock 하는 테스트가 같은 워커에 배치될 때 실제 모듈을 먼저 로드해 버린다.
60
+ it("0..23 을 벗어나면 거부한다", () => {
61
+ const fromDef = findSettingDef("slack.quietFromHour")!;
62
+ expect(validateSettingValue(fromDef, "24")).toMatchObject({ ok: false });
63
+ expect(validateSettingValue(fromDef, "-1")).toMatchObject({ ok: false });
64
+ expect(validateSettingValue(fromDef, "0")).toMatchObject({ ok: true });
65
+ expect(validateSettingValue(fromDef, "23")).toMatchObject({ ok: true });
66
+ });
67
+ });
68
+
40
69
  describe("maskSecret", () => {
41
70
  it("긴 값은 앞뒤만 남긴다", () => {
42
71
  expect(maskSecret("xoxb-1234567890-abcdefghij")).toBe("xoxb-123...ghij");
@@ -3,12 +3,21 @@
3
3
  *
4
4
  * 키·env 이름·타입·검증을 한 곳에 모아 API 와 UI 가 같은 정의를 본다. 여기 없는 env 는
5
5
  * 화면에서 바꿀 수 없다 — DB 접속 정보나 포트처럼 서버가 뜨기 전에 필요한 값들이다.
6
+ *
7
+ * `settings` 테이블은 `(key, value)` 문자열 쌍만 담는 그릇이라(`setting.entity.json`),
8
+ * "이 키가 정수인지·범위가 얼마인지·화면에 뭐라고 쓸지"를 담을 곳이 따로 필요하다.
9
+ * 그 메타데이터가 여기 있다 — DB 스키마가 아니라 앱이 아는 상수 목록이다.
6
10
  */
7
11
 
8
- export type SettingKind = "boolean" | "integer" | "number" | "string" | "secret";
12
+ export type SettingKind = "boolean" | "integer" | "number" | "string" | "secret" | "preset";
9
13
 
10
- /** 화면에서 묶어 보여줄 단위. 9개를 평평하게 나열하면 어디를 봐야 할지 알 수 없다. */
11
- export type SettingGroup = "openai" | "slack";
14
+ /**
15
+ * 화면에서 묶어 보여줄 단위. 평평하게 나열하면 어디를 봐야 할지 알 수 없다.
16
+ *
17
+ * Slack 은 "언제 보낼까"(slack)와 "어디로 보낼까"(slackConnection)를 나눈다 — 앞은 운영
18
+ * 중 자주 만지고 뒤는 한 번 맞추면 끝이라 수명이 다르다.
19
+ */
20
+ export type SettingGroup = "openai" | "slack" | "slackConnection";
12
21
 
13
22
  export type SettingDef = {
14
23
  key: string;
@@ -20,6 +29,8 @@ export type SettingDef = {
20
29
  applies: "immediate" | "restart";
21
30
  min?: number;
22
31
  max?: number;
32
+ /** `preset` 종류에서 고를 수 있는 값. 이 목록 밖의 값은 저장되지 않는다. */
33
+ presets?: number[];
23
34
  /**
24
35
  * 저장값도 env 도 없을 때 실제로 적용되는 값. 화면을 비워두면 "설정 안 됨"으로 읽히지만
25
36
  * 실제로는 이 값이 돌고 있다 — 소비처의 기본값과 반드시 같아야 한다.
@@ -90,22 +101,77 @@ export const SETTING_DEFS: SettingDef[] = [
90
101
  max: MAX_OPENAI_MIN_HOST_AVAILABLE_GIB,
91
102
  help: "호스트 여유가 이 값 아래면 스케일업을 멈춥니다. 확장을 끄려면 오토스케일을 끄세요",
92
103
  },
104
+ {
105
+ key: "slack.enabled",
106
+ fallback: "true",
107
+ group: "slack",
108
+ envKey: "SLACK_ENABLED",
109
+ label: "Slack 알림",
110
+ kind: "boolean",
111
+ applies: "immediate",
112
+ help: "연휴처럼 규칙으로 잡을 수 없는 기간에 끕니다. 서비스 정지급 장애 알림은 꺼도 전달됩니다",
113
+ },
114
+ {
115
+ key: "slack.remindersEnabled",
116
+ fallback: "true",
117
+ group: "slack",
118
+ envKey: "SLACK_REMINDERS_ENABLED",
119
+ label: "만료 알림 반복",
120
+ kind: "boolean",
121
+ applies: "immediate",
122
+ help: "끄면 반복만 멈추고 만료 순간의 알림은 그대로 갑니다",
123
+ },
93
124
  {
94
125
  key: "slack.expiryReminderMinutes",
95
- fallback: "0",
126
+ fallback: "60",
96
127
  group: "slack",
97
128
  envKey: "SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES",
98
- label: "만료 알림 반복 주기 (분)",
129
+ label: "반복 주기",
130
+ kind: "preset",
131
+ applies: "immediate",
132
+ // 자유 입력 대신 고정 선택지를 쓴다 — "얼마나 자주 재촉할까"라서 1분 단위 정밀도가
133
+ // 의미 없고, 잘못 넣으면 채널이 시끄러워진다. 끄는 것은 remindersEnabled 가 맡는다.
134
+ presets: [10, 30, 60, 180],
135
+ help: "재로그인이 필요한 토큰이 남아 있는 동안 이 주기로 다시 알립니다",
136
+ },
137
+ {
138
+ key: "slack.quietFromHour",
139
+ fallback: "20",
140
+ group: "slack",
141
+ envKey: "SLACK_QUIET_FROM_HOUR",
142
+ label: "조용 시간 시작",
143
+ kind: "integer",
144
+ applies: "immediate",
145
+ min: 0,
146
+ max: 23,
147
+ help: "이 시각부터 알리지 않습니다 (한국 시간)",
148
+ },
149
+ {
150
+ key: "slack.quietUntilHour",
151
+ fallback: "8",
152
+ group: "slack",
153
+ envKey: "SLACK_QUIET_UNTIL_HOUR",
154
+ label: "조용 시간 종료",
99
155
  kind: "integer",
100
156
  applies: "immediate",
101
157
  min: 0,
102
- max: 1440,
103
- help: "0 이면 반복 알림을 끕니다",
158
+ max: 23,
159
+ help: "이 시각부터 다시 알립니다. 시작보다 빠르면 자정을 넘는 구간이 됩니다",
160
+ },
161
+ {
162
+ key: "slack.notifyOnWeekends",
163
+ fallback: "false",
164
+ group: "slack",
165
+ envKey: "SLACK_NOTIFY_ON_WEEKENDS",
166
+ label: "주말에도 알림",
167
+ kind: "boolean",
168
+ applies: "immediate",
169
+ help: "켜면 주말에도 위의 조용 시간 규칙만 적용됩니다",
104
170
  },
105
171
  {
106
172
  key: "slack.channelId",
107
173
  fallback: "",
108
- group: "slack",
174
+ group: "slackConnection",
109
175
  envKey: "SLACK_CHANNEL_ID",
110
176
  label: "Slack 채널 ID",
111
177
  kind: "string",
@@ -115,7 +181,7 @@ export const SETTING_DEFS: SettingDef[] = [
115
181
  {
116
182
  key: "slack.userMap",
117
183
  fallback: "",
118
- group: "slack",
184
+ group: "slackConnection",
119
185
  envKey: "SLACK_USER_MAP",
120
186
  label: "Slack 사용자 매핑",
121
187
  kind: "string",
@@ -125,7 +191,7 @@ export const SETTING_DEFS: SettingDef[] = [
125
191
  {
126
192
  key: "slack.botToken",
127
193
  fallback: "",
128
- group: "slack",
194
+ group: "slackConnection",
129
195
  envKey: "SLACK_BOT_TOKEN",
130
196
  label: "Slack 봇 토큰",
131
197
  kind: "secret",
@@ -149,6 +215,16 @@ export function validateSettingValue(
149
215
  return { ok: true, value: trimmed };
150
216
  }
151
217
 
218
+ if (def.kind === "preset") {
219
+ // 화면은 버튼만 주지만 API 는 아무 값이나 받을 수 있다. 목록 밖의 값이 들어오면
220
+ // 화면에서 아무 버튼도 선택돼 보이지 않아 "왜 안 바뀌지"가 된다.
221
+ const parsed = Number(trimmed);
222
+ if (!def.presets?.includes(parsed)) {
223
+ return { ok: false, error: `${def.presets?.join(", ") ?? ""} 중 하나여야 합니다` };
224
+ }
225
+ return { ok: true, value: String(parsed) };
226
+ }
227
+
152
228
  if (def.kind === "integer" || def.kind === "number") {
153
229
  const parsed = Number(trimmed);
154
230
  if (!Number.isFinite(parsed)) return { ok: false, error: "숫자를 입력하세요" };
@@ -11,7 +11,7 @@ import {
11
11
  import { SD } from "../../i18n/sd.generated";
12
12
  import { type SettingSubsetKey, type SettingSubsetMapping } from "../sonamu.generated";
13
13
  import { settingSubsetQueries, settingLoaderQueries } from "../sonamu.generated.sso";
14
- import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.schema";
14
+ import { findSettingDef, maskSecret, SETTING_DEFS, validateSettingValue } from "./setting.constant";
15
15
  import {
16
16
  getSetting,
17
17
  isStored,
@@ -213,6 +213,7 @@ class SettingModelClass extends BaseModelClass<
213
213
  source,
214
214
  min: def.min ?? null,
215
215
  max: def.max ?? null,
216
+ presets: def.presets ?? [],
216
217
  help: def.help ?? null,
217
218
  };
218
219
  });
@@ -258,6 +259,19 @@ class SettingModelClass extends BaseModelClass<
258
259
  await resetStoredSetting(key);
259
260
  return { applies: def.applies };
260
261
  }
262
+
263
+ /**
264
+ * 만료 알림을 지금 한 번 보낸다. 주기를 기다리지 않고 설정이 제대로 붙었는지 확인하거나,
265
+ * 재로그인을 재촉할 때 쓴다. 보낸 건수를 돌려줘 "보낼 대상 없음"과 구분한다.
266
+ */
267
+ @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
268
+ async triggerExpiryReminder(): Promise<{ sent: number }> {
269
+ // 동적 import 로 알림 모듈을 호출 시점에만 끌어온다. 정적으로 걸면
270
+ // setting.model → expired-token-reminder → token.model 경로가 생겨,
271
+ // token.model 을 mock 하는 테스트가 실제 모듈을 먼저 로드하게 된다.
272
+ const { sendExpiredTokenReminderNow } = await import("../qgrid/expired-token-reminder");
273
+ return { sent: await sendExpiredTokenReminderNow() };
274
+ }
261
275
  }
262
276
 
263
277
  export const SettingModel = new SettingModelClass();
@@ -12,7 +12,7 @@ export type SettingSaveParams = z.infer<typeof SettingSaveParams>;
12
12
 
13
13
  // ── 설정 API 응답 스키마 ──
14
14
 
15
- export const SettingKind = z.enum(["boolean", "integer", "number", "string", "secret"]);
15
+ export const SettingKind = z.enum(["boolean", "integer", "number", "string", "secret", "preset"]);
16
16
  export type SettingKind = z.infer<typeof SettingKind>;
17
17
 
18
18
  export const SettingApplies = z.enum(["immediate", "restart"]);
@@ -33,6 +33,8 @@ export const SettingItem = z.object({
33
33
  source: SettingSource,
34
34
  min: z.number().nullable(),
35
35
  max: z.number().nullable(),
36
+ /** `preset` 종류에서 고를 수 있는 값. 다른 종류는 빈 배열. */
37
+ presets: z.number().array(),
36
38
  help: z.string().nullable(),
37
39
  });
38
40
  export type SettingItem = z.infer<typeof SettingItem>;
@@ -117,6 +117,13 @@ Content-Type: application/json
117
117
 
118
118
  ###
119
119
 
120
+ POST {{baseUrl}}/api/setting/triggerExpiryReminder
121
+ Content-Type: application/json
122
+
123
+ {}
124
+
125
+ ###
126
+
120
127
  GET {{baseUrl}}/api/requestLogStep/findById
121
128
  ?subset=A
122
129
  &id=0
@@ -176,7 +176,9 @@ export default defineConfig({
176
176
  await loadSettings();
177
177
  // 런타임 설정 소비자를 상위 계층에서 연결해 setting 모듈이 qgrid 를 역참조하지 않게 한다.
178
178
  setSettingChangeHandler((key) => {
179
- if (key === "slack.expiryReminderMinutes") rescheduleExpiredTokenReminder();
179
+ if (key === "slack.expiryReminderMinutes" || key === "slack.remindersEnabled") {
180
+ rescheduleExpiredTokenReminder();
181
+ }
180
182
  });
181
183
 
182
184
  let triggerReady = true;
@@ -2,7 +2,7 @@ import {
2
2
  MAX_OPENAI_ESTIMATED_RSS_GIB,
3
3
  MAX_OPENAI_MIN_HOST_AVAILABLE_GIB,
4
4
  SETTING_DEFS,
5
- } from "../../../application/setting/setting.schema";
5
+ } from "../../../application/setting/setting.constant";
6
6
  import { getSetting } from "../../../application/setting/setting.store";
7
7
 
8
8
  export const MAX_OPENAI_WORKERS_PER_TOKEN = 20;