kythia-core 0.11.0-beta → 0.11.1-beta

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kythia-core",
3
- "version": "0.11.0-beta",
3
+ "version": "0.11.1-beta",
4
4
  "description": "Core library for the Kythia main Discord bot: extensible, modular, and scalable foundation for commands, components, and event management.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Kythia.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @file src/Kythia.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * The heart of the application lifecycle. This class acts as the central
@@ -4,7 +4,7 @@
4
4
  * @file src/client/kythiaClient.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Factory function that initializes the Discord.js Client with high-performance
@@ -49,7 +49,6 @@ module.exports = function kythiaClient() {
49
49
 
50
50
  ThreadManager: {
51
51
  maxSize: 25,
52
- keepOverLimit: (thread) => thread.isActive(),
53
52
  },
54
53
 
55
54
  GuildMemberManager: {
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/Command.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * The base class for all Kythia CLI commands. It enforces a standard structure
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/CacheClearCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Interactive utility to flush Redis cache. Supports intelligent handling of
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/LangCheckCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Performs a deep AST analysis of the codebase to find `t()` translation function calls.
@@ -23,7 +23,25 @@ const glob = require('glob');
23
23
  const parser = require('@babel/parser');
24
24
  const traverse = require('@babel/traverse').default;
25
25
 
26
- function getAllKeys(obj, prefix = '') {
26
+ function deepMerge(target, source) {
27
+ if (typeof target !== 'object' || target === null) return source;
28
+ if (typeof source !== 'object' || source === null) return source;
29
+
30
+ for (const key of Object.keys(source)) {
31
+ if (
32
+ source[key] instanceof Object &&
33
+ target[key] instanceof Object &&
34
+ !Array.isArray(source[key])
35
+ ) {
36
+ target[key] = deepMerge(target[key], source[key]);
37
+ } else {
38
+ target[key] = source[key];
39
+ }
40
+ }
41
+ return target;
42
+ }
43
+
44
+ function getAllKeys(obj, allDefinedKeys, prefix = '') {
27
45
  Object.keys(obj).forEach((key) => {
28
46
  if (key === '_value' || key === 'text') {
29
47
  if (Object.keys(obj).length === 1) return;
@@ -33,7 +51,7 @@ function getAllKeys(obj, prefix = '') {
33
51
  const fullKey = prefix ? `${prefix}.${key}` : key;
34
52
  if (typeof obj[key] === 'object' && obj[key] !== null) {
35
53
  if (key !== 'jobs' && key !== 'shop') {
36
- getAllKeys(obj[key], fullKey);
54
+ getAllKeys(obj[key], allDefinedKeys, fullKey);
37
55
  } else {
38
56
  allDefinedKeys.add(fullKey);
39
57
  }
@@ -49,9 +67,8 @@ class LangCheckCommand extends Command {
49
67
  'Lint translation key usage in code and language files (AST-based)';
50
68
 
51
69
  async handle() {
52
- const PROJECT_ROOT = path.join(__dirname, '..', '..', '..');
70
+ const PROJECT_ROOT = process.cwd();
53
71
  const SCAN_DIRECTORIES = ['addons', 'src'];
54
- const LANG_DIR = path.join(PROJECT_ROOT, 'src', 'lang');
55
72
  const DEFAULT_LANG = 'en';
56
73
  const IGNORE_PATTERNS = [
57
74
  '**/node_modules/**',
@@ -94,41 +111,53 @@ class LangCheckCommand extends Command {
94
111
  }
95
112
 
96
113
  function _loadLocales() {
97
- console.log(`\n🔍 Reading language files from: ${LANG_DIR}`);
114
+ console.log(`\n🔍 Searching for language files in: ${PROJECT_ROOT}`);
98
115
  try {
99
- const langFiles = fs
100
- .readdirSync(LANG_DIR)
101
- .filter(
102
- (file) =>
103
- file.endsWith('.json') &&
104
- !file.includes('_flat') &&
105
- !file.includes('_FLAT'),
106
- );
116
+ const langFiles = glob.sync('**/lang/*.json', {
117
+ cwd: PROJECT_ROOT,
118
+ ignore: ['**/node_modules/**', '**/dist/**'],
119
+ absolute: true,
120
+ });
121
+
107
122
  if (langFiles.length === 0) {
108
123
  console.error(
109
124
  '\x1b[31m%s\x1b[0m',
110
- '❌ No .json files found in the language folder.',
125
+ '❌ No .json files found in any lang folder.',
111
126
  );
112
127
  return false;
113
128
  }
129
+
130
+ let loadedCount = 0;
114
131
  for (const file of langFiles) {
115
- const lang = file.replace('.json', '');
116
- const content = fs.readFileSync(path.join(LANG_DIR, file), 'utf8');
132
+ if (file.includes('_flat') || file.includes('_FLAT')) continue;
133
+
134
+ const filename = path.basename(file);
135
+ const lang = filename.replace('.json', '');
136
+ const content = fs.readFileSync(file, 'utf8');
137
+
117
138
  try {
118
- locales[lang] = JSON.parse(content);
119
- console.log(` > Successfully loaded: ${file}`);
139
+ const parsed = JSON.parse(content);
140
+ if (!locales[lang]) {
141
+ locales[lang] = parsed;
142
+ } else {
143
+ // Merge with existing locale data
144
+ locales[lang] = deepMerge(locales[lang], parsed);
145
+ }
146
+ loadedCount++;
120
147
  } catch (jsonError) {
121
148
  console.error(
122
149
  `\x1b[31m%s\x1b[0m`,
123
- `❌ Failed to parse JSON: ${file} - ${jsonError.message}`,
150
+ `❌ Failed to parse JSON: ${path.relative(PROJECT_ROOT, file)} - ${jsonError.message}`,
124
151
  );
125
152
  filesWithErrors++;
126
153
  }
127
154
  }
155
+ console.log(` > Successfully loaded ${loadedCount} language files.`);
156
+
128
157
  if (!locales[DEFAULT_LANG]) {
129
158
  console.error(
130
159
  `\x1b[31m%s\x1b[0m`,
131
- `❌ Default language (${DEFAULT_LANG}) not found!`,
160
+ `❌ Default language (${DEFAULT_LANG}) not found in any loaded files!`,
132
161
  );
133
162
  return false;
134
163
  }
@@ -303,7 +332,7 @@ class LangCheckCommand extends Command {
303
332
 
304
333
  if (defaultLocale) {
305
334
  try {
306
- getAllKeys(defaultLocale);
335
+ getAllKeys(defaultLocale, allDefinedKeys);
307
336
  } catch (e) {
308
337
  console.error('Error collecting defined keys:', e);
309
338
  }
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/LangTranslateCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Automates the translation of the core language file (`en.json`) to a target language
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/MakeMigrationCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Scaffolds a new database migration file with a precise YYYYMMDD_HHMMSS timestamp prefix.
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/MakeModelCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Generates new Sequelize model files extending `KythiaModel`.
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/MigrateCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Manages database schema updates using Umzug. Supports standard migration,
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/NamespaceCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Scans the entire project structure and automatically adds or updates
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/StructureCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Generates a markdown tree representation of the entire project directory.
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/commands/UpversionCommand.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Synchronizes the `@version` tag in all JSDoc headers across the project
package/src/cli/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * @file src/cli/index.js
7
7
  * @copyright © 2025 kenndeclouv
8
8
  * @assistant chaa & graa
9
- * @version 0.11.0-beta
9
+ * @version 0.11.1-beta
10
10
  *
11
11
  * @description
12
12
  * The main bootstrap entry point for the Kythia CLI.
@@ -4,7 +4,7 @@
4
4
  * @file src/cli/utils/db.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Initializes the database connection and migration engine (Umzug) specifically
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaMigrator.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Scans 'addons' folder for migration files and executes them using Umzug.
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaModel.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Caching layer for Sequelize Models, now sharding-aware. When config.db.redis.shard === true,
@@ -659,10 +659,6 @@ class KythiaModel extends Model {
659
659
  * @param {string|Object} queryIdentifier - A unique string or a Sequelize query object.
660
660
  * @returns {string} The final cache key, prefixed with the model's name (e.g., "User:{\"id\":1}").
661
661
  */
662
- /**
663
- * 🔑 Generates a consistent, model-specific cache key from a query identifier.
664
- * FIXED: Handle BigInt in json-stable-stringify
665
- */
666
662
  static getCacheKey(queryIdentifier) {
667
663
  let dataToHash = queryIdentifier;
668
664
 
@@ -676,7 +672,7 @@ class KythiaModel extends Model {
676
672
  }
677
673
 
678
674
  const opts = {
679
- replacer: (value) =>
675
+ replacer: (_key, value) =>
680
676
  typeof value === 'bigint' ? value.toString() : value,
681
677
  };
682
678
 
@@ -712,6 +708,38 @@ class KythiaModel extends Model {
712
708
  return normalized;
713
709
  }
714
710
 
711
+ /**
712
+ * 🧠 Generate tags based on PK and static cacheKeys definition.
713
+ * Used for smart invalidation (e.g. invalidate all items belonging to a userId).
714
+ */
715
+ static _generateSmartTags(instance) {
716
+ if (!instance) return [`${this.name}`];
717
+
718
+ const tags = [`${this.name}`];
719
+
720
+ const pk = this.primaryKeyAttribute;
721
+ if (instance[pk]) {
722
+ tags.push(`${this.name}:${pk}:${instance[pk]}`);
723
+ }
724
+
725
+ const smartKeys = this.cacheKeys || this.CACHE_KEYS || [];
726
+
727
+ if (Array.isArray(smartKeys)) {
728
+ for (const keyGroup of smartKeys) {
729
+ const keys = Array.isArray(keyGroup) ? keyGroup : [keyGroup];
730
+ const hasAllValues = keys.every(
731
+ (k) => instance[k] !== undefined && instance[k] !== null,
732
+ );
733
+
734
+ if (hasAllValues) {
735
+ const tagParts = keys.map((k) => `${k}:${instance[k]}`).join(':');
736
+ tags.push(`${this.name}:${tagParts}`);
737
+ }
738
+ }
739
+ }
740
+ return tags;
741
+ }
742
+
715
743
  /**
716
744
  * 📥 [HYBRID/SHARD ROUTER] Sets a value in the currently active cache engine.
717
745
  * In shard mode, if Redis down, nothing is cached.
@@ -774,6 +802,7 @@ class KythiaModel extends Model {
774
802
  static async _redisSetCacheEntry(cacheKey, data, ttl, tags = []) {
775
803
  try {
776
804
  let plainData = data;
805
+
777
806
  if (data && typeof data.toJSON === 'function') {
778
807
  plainData = data.toJSON();
779
808
  } else if (Array.isArray(data)) {
@@ -789,6 +818,7 @@ class KythiaModel extends Model {
789
818
 
790
819
  const multi = this.redis.multi();
791
820
  multi.set(cacheKey, valueToStore, 'PX', ttl);
821
+
792
822
  for (const tag of tags) {
793
823
  multi.sadd(tag, cacheKey);
794
824
  }
@@ -821,19 +851,21 @@ class KythiaModel extends Model {
821
851
  ? Array.isArray(includeOptions)
822
852
  ? includeOptions
823
853
  : [includeOptions]
824
- : null;
854
+ : undefined;
825
855
 
826
- if (Array.isArray(parsedData)) {
827
- const instances = this.bulkBuild(parsedData, {
856
+ const buildInstance = (data) => {
857
+ const instance = this.build(data, {
828
858
  isNewRecord: false,
829
859
  include: includeAsArray,
830
860
  });
861
+ return instance;
862
+ };
863
+
864
+ if (Array.isArray(parsedData)) {
865
+ const instances = parsedData.map((d) => buildInstance(d));
831
866
  return { hit: true, data: instances };
832
867
  } else {
833
- const instance = this.build(parsedData, {
834
- isNewRecord: false,
835
- include: includeAsArray,
836
- });
868
+ const instance = buildInstance(parsedData);
837
869
  return { hit: true, data: instance };
838
870
  }
839
871
  } catch (err) {
@@ -1055,14 +1087,6 @@ class KythiaModel extends Model {
1055
1087
  static async getCache(keys, options = {}) {
1056
1088
  const { noCache, customCacheKey, ttl, ...explicitQueryOptions } = options;
1057
1089
 
1058
- if (noCache) {
1059
- const queryToRun = {
1060
- ...this._normalizeFindOptions(keys),
1061
- ...explicitQueryOptions,
1062
- };
1063
- return this.findOne(queryToRun);
1064
- }
1065
-
1066
1090
  if (Array.isArray(keys)) {
1067
1091
  const pk = this.primaryKeyAttribute;
1068
1092
  return this.findAll({ where: { [pk]: keys.map((m) => m[pk]) } });
@@ -1074,11 +1098,15 @@ class KythiaModel extends Model {
1074
1098
  ...normalizedKeys,
1075
1099
  ...explicitQueryOptions,
1076
1100
  where: {
1077
- ...normalizedKeys.where,
1101
+ ...(normalizedKeys.where || {}),
1078
1102
  ...(explicitQueryOptions.where || {}),
1079
1103
  },
1080
1104
  };
1081
1105
 
1106
+ if (noCache) {
1107
+ return this.findOne(finalQuery);
1108
+ }
1109
+
1082
1110
  if (!finalQuery.where || Object.keys(finalQuery.where).length === 0) {
1083
1111
  return null;
1084
1112
  }
@@ -1094,7 +1122,7 @@ class KythiaModel extends Model {
1094
1122
  return this.pendingQueries.get(cacheKey);
1095
1123
 
1096
1124
  const queryPromise = this.findOne(finalQuery)
1097
- .then((record) => {
1125
+ .then(async (record) => {
1098
1126
  if (this.isRedisConnected || !this.isShardMode) {
1099
1127
  const tags = [`${this.name}`];
1100
1128
  if (record)
@@ -1104,7 +1132,7 @@ class KythiaModel extends Model {
1104
1132
  }`,
1105
1133
  );
1106
1134
 
1107
- this.setCacheEntry(cacheKey, record, ttl, tags);
1135
+ await this.setCacheEntry(cacheKey, record, ttl, tags);
1108
1136
  }
1109
1137
  return record;
1110
1138
  })
@@ -1142,12 +1170,12 @@ class KythiaModel extends Model {
1142
1170
  return this.pendingQueries.get(cacheKey);
1143
1171
 
1144
1172
  const queryPromise = this.findAll(normalizedOptions)
1145
- .then((records) => {
1173
+ .then(async (records) => {
1146
1174
  if (this.isRedisConnected || !this.isShardMode) {
1147
1175
  const tags = [`${this.name}`];
1148
1176
  if (Array.isArray(cacheTags)) tags.push(...cacheTags);
1149
1177
 
1150
- this.setCacheEntry(cacheKey, records, ttl, tags);
1178
+ await this.setCacheEntry(cacheKey, records, ttl, tags);
1151
1179
  }
1152
1180
  return records;
1153
1181
  })
@@ -1340,29 +1368,40 @@ class KythiaModel extends Model {
1340
1368
  }
1341
1369
 
1342
1370
  /**
1343
- * 📦 An instance method that saves the current model instance to the database and then
1344
- * intelligently updates its corresponding entry in the active cache.
1371
+ * 📦 FIXED: Save data to DB, then INVALIDATE the cache tags.
1372
+ * Don't try to setCache here, because .save() result doesn't have associations/includes.
1373
+ * Let the next getCache() fetch the full fresh data tree.
1345
1374
  */
1346
1375
  async saveAndUpdateCache() {
1347
1376
  const savedInstance = await this.save();
1348
1377
  const pk = this.constructor.primaryKeyAttribute;
1349
1378
  const pkValue = this[pk];
1379
+
1350
1380
  if (
1351
1381
  pkValue &&
1352
1382
  (this.constructor.isRedisConnected || !this.constructor.isShardMode)
1353
1383
  ) {
1354
- const cacheKey = this.constructor.getCacheKey({ [pk]: pkValue });
1384
+ const cacheKey = this.constructor.getCacheKey({
1385
+ where: { [pk]: pkValue },
1386
+ });
1387
+
1355
1388
  const tags = [
1356
1389
  `${this.constructor.name}`,
1357
1390
  `${this.constructor.name}:${pk}:${pkValue}`,
1358
1391
  ];
1392
+
1359
1393
  await this.constructor.setCacheEntry(
1360
1394
  cacheKey,
1361
1395
  savedInstance,
1362
1396
  undefined,
1363
1397
  tags,
1364
1398
  );
1399
+
1400
+ this.constructor.logger.info(
1401
+ `🔄 [CACHE] Updated cache for ${this.constructor.name}:${pk}:${pkValue}`,
1402
+ );
1365
1403
  }
1404
+
1366
1405
  return savedInstance;
1367
1406
  }
1368
1407
 
@@ -1413,16 +1452,11 @@ class KythiaModel extends Model {
1413
1452
  return;
1414
1453
  }
1415
1454
 
1416
- /**
1417
- * Logika setelah data disimpan (Create atau Update)
1418
- */
1419
1455
  const afterSaveLogic = async (instance) => {
1420
1456
  const modelClass = instance.constructor;
1421
1457
 
1422
1458
  if (modelClass.isRedisConnected) {
1423
- const tagsToInvalidate = [`${modelClass.name}`];
1424
- const pk = modelClass.primaryKeyAttribute;
1425
- tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
1459
+ const tagsToInvalidate = modelClass._generateSmartTags(instance);
1426
1460
 
1427
1461
  if (Array.isArray(modelClass.customInvalidationTags)) {
1428
1462
  tagsToInvalidate.push(...modelClass.customInvalidationTags);
@@ -1433,16 +1467,11 @@ class KythiaModel extends Model {
1433
1467
  }
1434
1468
  };
1435
1469
 
1436
- /**
1437
- * Logika setelah data dihapus
1438
- */
1439
1470
  const afterDestroyLogic = async (instance) => {
1440
1471
  const modelClass = instance.constructor;
1441
1472
 
1442
1473
  if (modelClass.isRedisConnected) {
1443
- const tagsToInvalidate = [`${modelClass.name}`];
1444
- const pk = modelClass.primaryKeyAttribute;
1445
- tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
1474
+ const tagsToInvalidate = modelClass._generateSmartTags(instance);
1446
1475
 
1447
1476
  if (Array.isArray(modelClass.customInvalidationTags)) {
1448
1477
  tagsToInvalidate.push(...modelClass.customInvalidationTags);
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaSequelize.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Main Sequelize connection factory for the application.
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaStorage.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Custom storage adapter for Umzug that mimics Laravel's migration table structure.
@@ -4,7 +4,7 @@
4
4
  * @file src/loaders/ModelLoader.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Scans the `addons` directory for KythiaModel definitions, requires them,
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/AddonManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all addon loading, command registration, and component management.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/EventManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord event listeners except InteractionCreate.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/InteractionManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord interaction events including slash commands, buttons, modals,
@@ -56,6 +56,10 @@ class InteractionManager {
56
56
  this.KythiaVoter = this.models.KythiaVoter;
57
57
  this.isTeam = this.helpers.discord.isTeam;
58
58
  this.isOwner = this.helpers.discord.isOwner;
59
+
60
+ if (!this.client.restartNoticeCooldowns) {
61
+ this.client.restartNoticeCooldowns = new Collection();
62
+ }
59
63
  }
60
64
 
61
65
  /**
@@ -305,6 +309,8 @@ class InteractionManager {
305
309
  } else {
306
310
  await command.execute(interaction);
307
311
  }
312
+
313
+ await this._checkRestartSchedule(interaction);
308
314
  } else {
309
315
  this.logger.error(
310
316
  "Command doesn't have a valid 'execute' function:",
@@ -637,6 +643,9 @@ class InteractionManager {
637
643
  if (this.container && !this.container.logger) {
638
644
  this.container.logger = this.logger;
639
645
  }
646
+
647
+ await this._checkRestartSchedule(interaction);
648
+
640
649
  await command.execute(interaction, this.container);
641
650
  }
642
651
 
@@ -706,6 +715,51 @@ class InteractionManager {
706
715
  }
707
716
  }
708
717
 
718
+ /**
719
+ * Check for scheduled restart and notify user
720
+ * @private
721
+ */
722
+ async _checkRestartSchedule(interaction) {
723
+ const restartTs = this.client.kythiaRestartTimestamp;
724
+
725
+ if (!restartTs || interaction.commandName === 'restart') return;
726
+
727
+ const userId = interaction.user.id;
728
+ const cooldowns = this.client.restartNoticeCooldowns;
729
+ const now = Date.now();
730
+ const cooldownTime = 5 * 60 * 1000;
731
+
732
+ if (cooldowns.has(userId)) {
733
+ const lastNotified = cooldowns.get(userId);
734
+ if (now - lastNotified < cooldownTime) {
735
+ return;
736
+ }
737
+ }
738
+
739
+ const timeLeft = restartTs - now;
740
+
741
+ if (timeLeft > 0) {
742
+ try {
743
+ const timeString = `<t:${Math.floor(restartTs / 1000)}:R>`;
744
+ // TODO: Add translation
745
+ const msg = `## ⚠️ System Notice\nKythia is scheduled to restart **${timeString}**.`;
746
+
747
+ if (interaction.replied || interaction.deferred) {
748
+ await interaction.followUp({
749
+ content: msg,
750
+ flags: MessageFlags.Ephemeral,
751
+ });
752
+
753
+ cooldowns.set(userId, now);
754
+
755
+ setTimeout(() => cooldowns.delete(userId), cooldownTime);
756
+ }
757
+ } catch (err) {
758
+ this.logger.error(err);
759
+ }
760
+ }
761
+ }
762
+
709
763
  /**
710
764
  * Handle interaction errors
711
765
  * @private
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/ShutdownManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.11.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles graceful shutdown procedures including interval tracking,