diff --git a/commands/applications/profileAdmin.js b/commands/applications/profileAdmin.js index de57224..15e6ba1 100644 --- a/commands/applications/profileAdmin.js +++ b/commands/applications/profileAdmin.js @@ -7,7 +7,11 @@ const { const { reconcileUserAchievements, reconcileAllAchievements, - auditProfileData + grantMaintenanceReward, + getMaintenanceRewardStatus, + auditProfileData, + DEFAULT_MAINTENANCE_REWARD_ID, + DEFAULT_MAINTENANCE_REWARD_AMOUNT } = require( "../../utils/profileMaintenance" ); @@ -67,6 +71,69 @@ module.exports = { .setDescription( "Audit stored profile statistics for inconsistencies" ) + ) + .addSubcommand( + subcommand => + subcommand + .setName( + "maintenance-reward" + ) + .setDescription( + "Grant an idempotent maintenance reward to stored profile users" + ) + .addStringOption( + option => + option + .setName( + "reward-id" + ) + .setDescription( + `Unique reward ID; defaults to ${DEFAULT_MAINTENANCE_REWARD_ID}` + ) + ) + .addIntegerOption( + option => + option + .setName( + "amount" + ) + .setDescription( + `Cookies per player; defaults to ${DEFAULT_MAINTENANCE_REWARD_AMOUNT}` + ) + .setMinValue( + 1 + ) + ) + .addBooleanOption( + option => + option + .setName( + "dry-run" + ) + .setDescription( + "Preview recipients without granting Cookies" + ) + ) + ) + .addSubcommand( + subcommand => + subcommand + .setName( + "maintenance-status" + ) + .setDescription( + "Show maintenance reward claim status" + ) + .addStringOption( + option => + option + .setName( + "reward-id" + ) + .setDescription( + `Reward ID; defaults to ${DEFAULT_MAINTENANCE_REWARD_ID}` + ) + ) ), async execute( @@ -98,31 +165,87 @@ module.exports = { }); - if ( - subcommand === - "reconcile" - ) { - const user = - interaction.options.getUser( - "user" - ); + try { + if ( + subcommand === + "reconcile" + ) { + const user = + interaction.options.getUser( + "user" + ); - const dryRun = - interaction.options.getBoolean( - "dry-run" - ) ?? false; + const dryRun = + interaction.options.getBoolean( + "dry-run" + ) ?? false; - if (user) { - const result = - await reconcileUserAchievements({ + if (user) { + const result = + await reconcileUserAchievements({ + guildId: + interaction.guildId, + + userId: + user.id, + + dryRun + }); + + + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setTitle( + "๐Ÿ› ๏ธ Profile Reconciliation" + ) + .setDescription( + `${dryRun ? "Dry run for" : "Reconciled"} ${user}.` + ) + .addFields( + { + name: + "New unlocks", + + value: + `**${result.unlocked}**`, + + inline: + true + }, + { + name: + "Definitions checked", + + value: + `**${result.checked}**`, + + inline: + true + }, + { + name: + "Event-only skipped", + + value: + `**${result.skippedEventOnly}**`, + + inline: + true + } + ) + ] + }); + } + + + const summary = + await reconcileAllAchievements({ guildId: interaction.guildId, - userId: - user.id, - dryRun }); @@ -134,35 +257,35 @@ module.exports = { "๐Ÿ› ๏ธ Profile Reconciliation" ) .setDescription( - `${dryRun ? "Dry run for" : "Reconciled"} ${user}.` + `${dryRun ? "Dry run completed" : "Reconciliation completed"} for stored profile users.` ) .addFields( + { + name: + "Users", + + value: + `**${summary.users}**`, + + inline: + true + }, { name: "New unlocks", value: - `**${result.unlocked}**`, + `**${summary.unlocked}**`, inline: true }, { name: - "Definitions checked", + "Event-only checks skipped", value: - `**${result.checked}**`, - - inline: - true - }, - { - name: - "Event-only skipped", - - value: - `**${result.skippedEventOnly}**`, + `**${summary.skippedEventOnly}**`, inline: true @@ -173,152 +296,334 @@ module.exports = { } - const summary = - await reconcileAllAchievements({ - guildId: - interaction.guildId, + if ( + subcommand === + "maintenance-reward" + ) { + const rewardId = + interaction.options.getString( + "reward-id" + ) + ?? + DEFAULT_MAINTENANCE_REWARD_ID; - dryRun + + const amount = + interaction.options.getInteger( + "amount" + ) + ?? + DEFAULT_MAINTENANCE_REWARD_AMOUNT; + + + /* + * Safer admin default: + * + * If omitted, maintenance-reward previews + * instead of paying immediately. + * + * Pass dry-run:false explicitly to execute. + */ + + const dryRun = + interaction.options.getBoolean( + "dry-run" + ) + ?? + true; + + + const summary = + await grantMaintenanceReward({ + guildId: + interaction.guildId, + + rewardId, + + amount, + + dryRun + }); + + + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setTitle( + dryRun + ? "๐Ÿงช Maintenance Reward Dry Run" + : "๐ŸŽ Maintenance Reward" + ) + .setDescription( + dryRun + ? "No Cookies were granted. Run again with `dry-run:false` to execute this reward." + : `Maintenance compensation **${rewardId}** has been processed.` + ) + .addFields( + { + name: + "Reward ID", + + value: + `\`${summary.rewardId}\``, + + inline: + false + }, + { + name: + "Per player", + + value: + `**${summary.amount.toLocaleString()} ๐Ÿช**`, + + inline: + true + }, + { + name: + "Stored profiles", + + value: + `**${summary.users}**`, + + inline: + true + }, + { + name: + dryRun + ? "Would receive" + : "Granted", + + value: + `**${dryRun ? summary.eligible : summary.granted}**`, + + inline: + true + }, + { + name: + "Already claimed", + + value: + `**${summary.alreadyClaimed}**`, + + inline: + true + }, + { + name: + "Failed", + + value: + `**${summary.failed}**`, + + inline: + true + } + ) + ] }); + } + + + if ( + subcommand === + "maintenance-status" + ) { + const rewardId = + interaction.options.getString( + "reward-id" + ) + ?? + DEFAULT_MAINTENANCE_REWARD_ID; + + + const status = + await getMaintenanceRewardStatus({ + guildId: + interaction.guildId, + + rewardId + }); + + + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setTitle( + "๐ŸŽ Maintenance Reward Status" + ) + .addFields( + { + name: + "Reward ID", + + value: + `\`${status.rewardId}\``, + + inline: + false + }, + { + name: + "Default amount", + + value: + `**${status.amount.toLocaleString()} ๐Ÿช**`, + + inline: + true + }, + { + name: + "Stored profiles", + + value: + `**${status.users}**`, + + inline: + true + }, + { + name: + "Claimed", + + value: + `**${status.claimed}**`, + + inline: + true + }, + { + name: + "Remaining", + + value: + `**${status.remaining}**`, + + inline: + true + } + ) + ] + }); + } + + + const audit = + await auditProfileData( + interaction.guildId + ); + + + const issuePreview = + audit.issues + .slice( + 0, + 10 + ) + .join( + "\n" + ); return interaction.editReply({ embeds: [ new EmbedBuilder() .setTitle( - "๐Ÿ› ๏ธ Profile Reconciliation" - ) - .setDescription( - `${dryRun ? "Dry run completed" : "Reconciliation completed"} for stored profile users.` + "๐Ÿ”Ž Profile Data Audit" ) .addFields( { name: - "Users", + "Stats documents", value: - `**${summary.users}**`, + `**${audit.statDocuments}**`, inline: true }, { name: - "New unlocks", + "Achievement documents", value: - `**${summary.unlocked}**`, + `**${audit.achievementDocuments}**`, inline: true }, { name: - "Event-only checks skipped", + "Active achievements", value: - `**${summary.skippedEventOnly}**`, + `**${audit.activeAchievements}**`, + + inline: + true + }, + { + name: + "Global badges", + + value: + `**${audit.badgeDocuments}** (${audit.claimedBadges} claimed)`, + + inline: + true + }, + { + name: + "Orphan achievement docs", + + value: + `**${audit.orphanAchievementDocs}**`, + + inline: + true + }, + { + name: + "Issues", + + value: + `**${audit.issueCount}**`, inline: true } ) + .setDescription( + issuePreview + ? `First issues:\n${issuePreview}` + : "No structural inconsistencies found." + ) ] }); } - - const audit = - await auditProfileData( - interaction.guildId + catch (error) { + console.error( + `[ProfileAdmin] ${subcommand} failed:`, + error ); - const issuePreview = - audit.issues - .slice( - 0, - 10 - ) - .join( - "\n" - ); - - - return interaction.editReply({ - embeds: [ - new EmbedBuilder() - .setTitle( - "๐Ÿ”Ž Profile Data Audit" - ) - .addFields( - { - name: - "Stats documents", - - value: - `**${audit.statDocuments}**`, - - inline: - true - }, - { - name: - "Achievement documents", - - value: - `**${audit.achievementDocuments}**`, - - inline: - true - }, - { - name: - "Active achievements", - - value: - `**${audit.activeAchievements}**`, - - inline: - true - }, - { - name: - "Global badges", - - value: - `**${audit.badgeDocuments}** (${audit.claimedBadges} claimed)`, - - inline: - true - }, - { - name: - "Orphan achievement docs", - - value: - `**${audit.orphanAchievementDocs}**`, - - inline: - true - }, - { - name: - "Issues", - - value: - `**${audit.issueCount}**`, - - inline: - true - } - ) - .setDescription( - issuePreview - ? `First issues:\n${issuePreview}` - : "No structural inconsistencies found." - ) - ] - }); + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setTitle( + "โŒ Profile Admin Error" + ) + .setDescription( + "The maintenance action failed. Check the bot logs for details." + ) + ] + }); + } } -}; \ No newline at end of file +}; diff --git a/utils/profileSystem.js b/utils/profileSystem.js index b0691d9..5327426 100644 --- a/utils/profileSystem.js +++ b/utils/profileSystem.js @@ -2929,6 +2929,10 @@ function formatCategoryName( "๐Ÿ… Other", activity: "๐Ÿ“… Activity", + vault: + "๐Ÿฆ Vault", + victim: + "๐ŸŽฏ Victim", }; return labels[category] ??