feat: Implement stateful admin update with post-restart context, database migrations, and dedicated view components.

This commit is contained in:
syntaxbullet
2025-12-24 14:03:15 +01:00
parent e084b6fa4e
commit e2aa5ee760
4 changed files with 436 additions and 154 deletions

View File

@@ -0,0 +1,100 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import { createInfoEmbed, createSuccessEmbed, createWarningEmbed, createErrorEmbed } from "@lib/embeds";
// Constants for UI
const LOG_TRUNCATE_LENGTH = 1000;
const OUTPUT_TRUNCATE_LENGTH = 500;
function truncate(text: string, maxLength: number): string {
return text.length > maxLength ? `${text.substring(0, maxLength)}\n...and more` : text;
}
export function getCheckingEmbed() {
return createInfoEmbed("Checking for updates...", "System Update");
}
export function getNoUpdatesEmbed() {
return createSuccessEmbed("The bot is already up to date.", "No Updates Found");
}
export function getUpdatesAvailableMessage(branch: string, log: string, force: boolean) {
const embed = createInfoEmbed(
`**Branch:** \`${branch}\`\n\n**Pending Changes:**\n\`\`\`\n${truncate(log, LOG_TRUNCATE_LENGTH)}\n\`\`\`\n**Do you want to proceed?**`,
"Updates Available"
);
const confirmButton = new ButtonBuilder()
.setCustomId("confirm_update")
.setLabel(force ? "Force Update & Restart" : "Update & Restart")
.setStyle(force ? ButtonStyle.Danger : ButtonStyle.Success);
const cancelButton = new ButtonBuilder()
.setCustomId("cancel_update")
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(confirmButton, cancelButton);
return { embeds: [embed], components: [row] };
}
export function getPreparingEmbed() {
return createInfoEmbed("⏳ Preparing update...", "Update In Progress");
}
export function getUpdatingEmbed(needsDependencyInstall: boolean) {
const message = `Downloading and applying updates...${needsDependencyInstall ? `\nExpect a slightly longer startup for dependency installation.` : ""}\nThe system will restart automatically.`;
return createWarningEmbed(message, "Updating & Restarting");
}
export function getCancelledEmbed() {
return createInfoEmbed("Update cancelled.", "Cancelled");
}
export function getTimeoutEmbed() {
return createWarningEmbed("Update confirmation timed out.", "Timed Out");
}
export function getErrorEmbed(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return createErrorEmbed(`Failed to update:\n\`\`\`\n${message}\n\`\`\``, "Update Failed");
}
export interface PostRestartResult {
installSuccess: boolean;
installOutput: string;
migrationSuccess: boolean;
migrationOutput: string;
ranInstall: boolean;
ranMigrations: boolean;
}
export function getPostRestartEmbed(result: PostRestartResult) {
const parts: string[] = ["System updated successfully."];
if (result.ranInstall) {
parts.push(`**Dependencies:** ${result.installSuccess ? "✅ Installed" : "❌ Failed"}`);
}
if (result.ranMigrations) {
parts.push(`**Migrations:** ${result.migrationSuccess ? "✅ Applied" : "❌ Failed"}`);
}
if (result.installOutput) {
parts.push(`\n**Install Output:**\n\`\`\`\n${truncate(result.installOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``);
}
if (result.migrationOutput) {
parts.push(`\n**Migration Output:**\n\`\`\`\n${truncate(result.migrationOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``);
}
const isSuccess = result.installSuccess && result.migrationSuccess;
const title = isSuccess ? "Update Complete" : "Update Completed with Errors";
return createSuccessEmbed(parts.join("\n"), title);
}
export function getInstallingDependenciesEmbed() {
return createSuccessEmbed("Installing dependencies...", "Post-Update Action");
}