diff --git a/bot/commands/admin/update.ts b/bot/commands/admin/update.ts index 90a5340..f0598e5 100644 --- a/bot/commands/admin/update.ts +++ b/bot/commands/admin/update.ts @@ -97,6 +97,7 @@ async function handleUpdate(interaction: any) { timestamp: Date.now(), runMigrations: requirements.needsMigrations, installDependencies: requirements.needsRootInstall || requirements.needsWebInstall, + buildWebAssets: requirements.needsWebBuild, previousCommit: previousCommit.substring(0, 7), newCommit: updateInfo.latestCommit }); diff --git a/bot/modules/admin/update.types.ts b/bot/modules/admin/update.types.ts index 633a6ad..8098d38 100644 --- a/bot/modules/admin/update.types.ts +++ b/bot/modules/admin/update.types.ts @@ -5,6 +5,7 @@ export interface RestartContext { timestamp: number; runMigrations: boolean; installDependencies: boolean; + buildWebAssets: boolean; previousCommit: string; newCommit: string; } @@ -12,6 +13,7 @@ export interface RestartContext { export interface UpdateCheckResult { needsRootInstall: boolean; needsWebInstall: boolean; + needsWebBuild: boolean; needsMigrations: boolean; changedFiles: string[]; error?: Error; diff --git a/bot/modules/admin/update.view.ts b/bot/modules/admin/update.view.ts index 734d37b..bb74e53 100644 --- a/bot/modules/admin/update.view.ts +++ b/bot/modules/admin/update.view.ts @@ -31,7 +31,7 @@ export function getUpdatesAvailableMessage( force: boolean ) { const { branch, currentCommit, latestCommit, commitCount, commits } = updateInfo; - const { needsRootInstall, needsWebInstall, needsMigrations } = requirements; + const { needsRootInstall, needsWebInstall, needsWebBuild, needsMigrations } = requirements; // Build commit list (max 5) const commitList = commits @@ -50,6 +50,7 @@ export function getUpdatesAvailableMessage( const reqs: string[] = []; if (needsRootInstall) reqs.push("šŸ“¦ Install root dependencies"); if (needsWebInstall) reqs.push("🌐 Install web dependencies"); + if (needsWebBuild) reqs.push("šŸ—ļø Build web dashboard"); if (needsMigrations) reqs.push("šŸ—ƒļø Run database migrations"); if (reqs.length === 0) reqs.push("⚔ Quick update (no extra steps)"); @@ -124,6 +125,9 @@ export function getUpdatingEmbed(requirements: UpdateCheckResult) { if (requirements.needsRootInstall || requirements.needsWebInstall) { steps.push("šŸ“¦ Dependencies will be installed after restart"); } + if (requirements.needsWebBuild) { + steps.push("šŸ—ļø Web dashboard will be rebuilt after restart"); + } if (requirements.needsMigrations) { steps.push("šŸ—ƒļø Migrations will run after restart"); } @@ -157,16 +161,19 @@ export function getErrorEmbed(error: unknown) { export interface PostRestartResult { installSuccess: boolean; installOutput: string; + webBuildSuccess: boolean; + webBuildOutput: string; migrationSuccess: boolean; migrationOutput: string; ranInstall: boolean; + ranWebBuild: boolean; ranMigrations: boolean; previousCommit?: string; newCommit?: string; } export function getPostRestartEmbed(result: PostRestartResult, hasRollback: boolean) { - const isSuccess = result.installSuccess && result.migrationSuccess; + const isSuccess = result.installSuccess && result.webBuildSuccess && result.migrationSuccess; const embed = new EmbedBuilder() .setTitle(isSuccess ? "āœ… Update Complete" : "āš ļø Update Completed with Issues") @@ -192,6 +199,13 @@ export function getPostRestartEmbed(result: PostRestartResult, hasRollback: bool ); } + if (result.ranWebBuild) { + results.push(result.webBuildSuccess + ? "āœ… Web dashboard built" + : "āŒ Web dashboard build failed" + ); + } + if (result.ranMigrations) { results.push(result.migrationSuccess ? "āœ… Migrations applied" @@ -216,6 +230,14 @@ export function getPostRestartEmbed(result: PostRestartResult, hasRollback: bool }); } + if (result.webBuildOutput && !result.webBuildSuccess) { + embed.addFields({ + name: "Web Build Output", + value: `\`\`\`\n${truncate(result.webBuildOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``, + inline: false + }); + } + if (result.migrationOutput && !result.migrationSuccess) { embed.addFields({ name: "Migration Output", @@ -259,6 +281,66 @@ export function getRunningMigrationsEmbed() { ); } +export function getBuildingWebEmbed() { + return createInfoEmbed( + "🌐 Building web dashboard assets...\nThis may take a moment.", + "ā³ Building Web Dashboard" + ); +} + +export interface PostRestartProgress { + installDeps: boolean; + buildWeb: boolean; + runMigrations: boolean; + currentStep: "starting" | "install" | "build" | "migrate" | "done"; + installDone?: boolean; + buildDone?: boolean; + migrateDone?: boolean; +} + +export function getPostRestartProgressEmbed(progress: PostRestartProgress) { + const steps: string[] = []; + + // Installation step + if (progress.installDeps) { + if (progress.currentStep === "install") { + steps.push("ā³ Installing dependencies..."); + } else if (progress.installDone) { + steps.push("āœ… Dependencies installed"); + } else { + steps.push("⬚ Install dependencies"); + } + } + + // Web build step + if (progress.buildWeb) { + if (progress.currentStep === "build") { + steps.push("ā³ Building web dashboard..."); + } else if (progress.buildDone) { + steps.push("āœ… Web dashboard built"); + } else { + steps.push("⬚ Build web dashboard"); + } + } + + // Migrations step + if (progress.runMigrations) { + if (progress.currentStep === "migrate") { + steps.push("ā³ Running migrations..."); + } else if (progress.migrateDone) { + steps.push("āœ… Migrations applied"); + } else { + steps.push("⬚ Run migrations"); + } + } + + if (steps.length === 0) { + steps.push("⚔ Quick restart (no extra steps needed)"); + } + + return createInfoEmbed(steps.join("\n"), "šŸ”„ Post-Update Tasks"); +} + export function getRollbackSuccessEmbed(commit: string) { return createSuccessEmbed( `Successfully rolled back to commit \`${commit}\`.\nThe bot will restart now.`, diff --git a/shared/modules/admin/update.service.ts b/shared/modules/admin/update.service.ts index 2b4f1c2..11e4dcd 100644 --- a/shared/modules/admin/update.service.ts +++ b/shared/modules/admin/update.service.ts @@ -2,7 +2,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import { writeFile, readFile, unlink } from "fs/promises"; import { Client, TextChannel } from "discord.js"; -import { getPostRestartEmbed, getInstallingDependenciesEmbed, getRunningMigrationsEmbed } from "@/modules/admin/update.view"; +import { getPostRestartEmbed, getPostRestartProgressEmbed, type PostRestartProgress } from "@/modules/admin/update.view"; import type { PostRestartResult } from "@/modules/admin/update.view"; import type { RestartContext, UpdateCheckResult, UpdateInfo, CommitInfo } from "@/modules/admin/update.types"; @@ -70,6 +70,14 @@ export class UpdateService { file === "web/package.json" || file === "web/bun.lock" ); + // Detect if web source files changed (requires rebuild) + const needsWebBuild = changedFiles.some(file => + file.startsWith("web/src/") || + file === "web/build.ts" || + file === "web/tailwind.config.ts" || + file === "web/tsconfig.json" + ); + const needsMigrations = changedFiles.some(file => file.includes("schema.ts") || file.startsWith("drizzle/") ); @@ -77,6 +85,7 @@ export class UpdateService { return { needsRootInstall, needsWebInstall, + needsWebBuild, needsMigrations, changedFiles }; @@ -85,6 +94,7 @@ export class UpdateService { return { needsRootInstall: false, needsWebInstall: false, + needsWebBuild: false, needsMigrations: false, changedFiles: [], error: e instanceof Error ? e : new Error(String(e)) @@ -259,43 +269,100 @@ export class UpdateService { const result: PostRestartResult = { installSuccess: true, installOutput: "", + webBuildSuccess: true, + webBuildOutput: "", migrationSuccess: true, migrationOutput: "", ranInstall: context.installDependencies, + ranWebBuild: context.buildWebAssets, ranMigrations: context.runMigrations, previousCommit: context.previousCommit, newCommit: context.newCommit }; + // Track progress for consolidated message + const progress: PostRestartProgress = { + installDeps: context.installDependencies, + buildWeb: context.buildWebAssets, + runMigrations: context.runMigrations, + currentStep: "starting" + }; + + // Only send progress message if there are tasks to run + const hasTasks = context.installDependencies || context.buildWebAssets || context.runMigrations; + let progressMessage = hasTasks + ? await channel.send({ embeds: [getPostRestartProgressEmbed(progress)] }) + : null; + + // Helper to update progress message + const updateProgress = async () => { + if (progressMessage) { + await progressMessage.edit({ embeds: [getPostRestartProgressEmbed(progress)] }); + } + }; + // 1. Install Dependencies if needed if (context.installDependencies) { try { - await channel.send({ embeds: [getInstallingDependenciesEmbed()] }); + progress.currentStep = "install"; + await updateProgress(); const { stdout: rootOutput } = await execAsync("bun install"); const { stdout: webOutput } = await execAsync("cd web && bun install"); result.installOutput = `šŸ“¦ Root: ${rootOutput.trim() || "Done"}\n🌐 Web: ${webOutput.trim() || "Done"}`; + progress.installDone = true; } catch (err: unknown) { result.installSuccess = false; result.installOutput = err instanceof Error ? err.message : String(err); + progress.installDone = true; // Mark as done even on failure console.error("Dependency Install Failed:", err); } } - // 2. Run Migrations + // 2. Build Web Assets if needed + if (context.buildWebAssets) { + try { + progress.currentStep = "build"; + await updateProgress(); + + const { stdout } = await execAsync("cd web && bun run build"); + result.webBuildOutput = stdout.trim() || "Build completed successfully"; + progress.buildDone = true; + } catch (err: unknown) { + result.webBuildSuccess = false; + result.webBuildOutput = err instanceof Error ? err.message : String(err); + progress.buildDone = true; + console.error("Web Build Failed:", err); + } + } + + // 3. Run Migrations if (context.runMigrations) { try { - await channel.send({ embeds: [getRunningMigrationsEmbed()] }); + progress.currentStep = "migrate"; + await updateProgress(); + const { stdout } = await execAsync("bun x drizzle-kit migrate"); result.migrationOutput = stdout; + progress.migrateDone = true; } catch (err: unknown) { result.migrationSuccess = false; result.migrationOutput = err instanceof Error ? err.message : String(err); + progress.migrateDone = true; console.error("Migration Failed:", err); } } + // Delete progress message before final result + if (progressMessage) { + try { + await progressMessage.delete(); + } catch { + // Message may already be deleted, ignore + } + } + return result; }