13 Commits

Author SHA1 Message Date
syntaxbullet
05f27ca604 refactor: fix frontend 2026-01-08 17:01:36 +01:00
syntaxbullet
d37059d50f chore: remove tickets from future commits 2026-01-08 16:45:49 +01:00
syntaxbullet
caafe6b34d refactor: update graphics paths 2026-01-08 16:42:14 +01:00
syntaxbullet
017f5ad818 refactor: fix stale imports 2026-01-08 16:39:34 +01:00
syntaxbullet
f92415b89c refactor: move drizzle to shared 2026-01-08 16:29:31 +01:00
syntaxbullet
3f028eb76a refactor: consolidate config loading 2026-01-08 16:21:25 +01:00
syntaxbullet
2b641c952d refactor: move config loading to shared directory 2026-01-08 16:15:55 +01:00
syntaxbullet
88b266f81b refactor: initial moves 2026-01-08 16:09:26 +01:00
syntaxbullet
53a2f1ff0c chore: combine processes 2026-01-08 15:13:09 +01:00
syntaxbullet
dc15212ecf web: mock dashboard 2026-01-08 14:49:59 +01:00
syntaxbullet
99e847175e chore: remove frontend boilerplate 2026-01-08 14:26:16 +01:00
syntaxbullet
b2c7fa6e83 feat: improvements to update command 2026-01-08 14:13:24 +01:00
syntaxbullet
9e7f18787b feat: improvements to web dashboard 2026-01-08 13:56:25 +01:00
195 changed files with 2915 additions and 1311 deletions

7
.gitignore vendored
View File

@@ -1,7 +1,8 @@
.env
node_modules
db-logs
db-data
shared/db-logs
shared/db/data
shared/db/loga
.cursor
# dependencies (bun install)
@@ -44,4 +45,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
src/db/data
src/db/log
scratchpad/
tickets/

View File

@@ -2,16 +2,20 @@ FROM oven/bun:latest AS base
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y git
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
# Install dependencies
# Install root project dependencies
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Install web project dependencies
COPY web/package.json web/bun.lock ./web/
RUN cd web && bun install --frozen-lockfile
# Copy source code
COPY . .
# Expose port
# Expose ports (3000 for web dashboard)
EXPOSE 3000
# Default command

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ModerationService } from "@/modules/moderation/moderation.service";
import { ModerationService } from "@shared/modules/moderation/moderation.service";
import { getCaseEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
export const moderationCase = createCommand({

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ModerationService } from "@/modules/moderation/moderation.service";
import { ModerationService } from "@shared/modules/moderation/moderation.service";
import { getCasesListEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
export const cases = createCommand({

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ModerationService } from "@/modules/moderation/moderation.service";
import { ModerationService } from "@shared/modules/moderation/moderation.service";
import { getClearSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
export const clearwarning = createCommand({

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ModalSubmitInteraction } from "discord.js";
import { config, saveConfig } from "@lib/config";
import type { GameConfigType } from "@lib/config";
import { config, saveConfig } from "@shared/lib/config";
import type { GameConfigType } from "@shared/lib/config";
import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
export const configCommand = createCommand({

View File

@@ -1,8 +1,8 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, AttachmentBuilder } from "discord.js";
import { config, saveConfig } from "@/lib/config";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { items } from "@/db/schema";
import { config, saveConfig } from "@shared/lib/config";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { items } from "@db/schema";
import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
export const createColor = createCommand({

View File

@@ -1,4 +1,4 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { renderWizard } from "@/modules/admin/item_wizard";

View File

@@ -1,8 +1,7 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { createBaseEmbed } from "@lib/embeds";
import { configManager } from "@/lib/configManager";
import { config, reloadConfig } from "@/lib/config";
import { config, reloadConfig, toggleCommand } from "@shared/lib/config";
import { AuroraClient } from "@/lib/BotClient";
export const features = createCommand({
@@ -79,11 +78,11 @@ export const features = createCommand({
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
configManager.toggleCommand(commandName, enabled);
toggleCommand(commandName, enabled);
await interaction.editReply({ content: `✅ Command **${commandName}** has been ${enabled ? "enabled" : "disabled"}. Reloading configuration...` });
// Reload config from disk (which was updated by configManager)
// Reload config from disk (which was updated by toggleCommand)
reloadConfig();
await AuroraClient.loadCommands(true);

View File

@@ -5,7 +5,7 @@ import { AuroraClient } from "@/lib/BotClient";
// Mock DrizzleClient
const executeMock = mock(() => Promise.resolve());
mock.module("@/lib/DrizzleClient", () => ({
mock.module("@shared/db/DrizzleClient", () => ({
DrizzleClient: {
execute: executeMock
}

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@lib/utils";
import { createCommand } from "@shared/lib/utils";
import { AuroraClient } from "@/lib/BotClient";
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, Colors } from "discord.js";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { sql } from "drizzle-orm";
import { createBaseEmbed } from "@lib/embeds";

View File

@@ -1,4 +1,4 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import {
SlashCommandBuilder,
ActionRowBuilder,
@@ -8,12 +8,12 @@ import {
PermissionFlagsBits,
MessageFlags
} from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { createSuccessEmbed, createErrorEmbed, createBaseEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors";
import { items } from "@/db/schema";
import { items } from "@db/schema";
import { ilike, isNotNull, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { getShopListingMessage } from "@/modules/economy/shop.view";
export const listing = createCommand({

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ModerationService } from "@/modules/moderation/moderation.service";
import { CaseType } from "@/lib/constants";
import { ModerationService } from "@shared/modules/moderation/moderation.service";
import { CaseType } from "@shared/lib/constants";
import { getNoteSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
export const note = createCommand({

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ModerationService } from "@/modules/moderation/moderation.service";
import { ModerationService } from "@shared/modules/moderation/moderation.service";
import { getCasesListEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
export const notes = createCommand({

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ComponentType } from "discord.js";
import { config } from "@/lib/config";
import { PruneService } from "@/modules/moderation/prune.service";
import { config } from "@shared/lib/config";
import { PruneService } from "@shared/modules/moderation/prune.service";
import {
getConfirmationMessage,
getProgressEmbed,

View File

@@ -1,4 +1,4 @@
import { createCommand } from "@lib/utils";
import { createCommand } from "@shared/lib/utils";
import { AuroraClient } from "@/lib/BotClient";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, TextChannel } from "discord.js";
import { terminalService } from "@/modules/terminal/terminal.service";
import { terminalService } from "@shared/modules/terminal/terminal.service";
import { createBaseEmbed, createErrorEmbed } from "@/lib/embeds";
export const terminal = createCommand({

View File

@@ -0,0 +1,176 @@
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ComponentType } from "discord.js";
import { UpdateService } from "@shared/modules/admin/update.service";
import {
getCheckingEmbed,
getNoUpdatesEmbed,
getUpdatesAvailableMessage,
getPreparingEmbed,
getUpdatingEmbed,
getCancelledEmbed,
getTimeoutEmbed,
getErrorEmbed,
getRollbackSuccessEmbed,
getRollbackFailedEmbed
} from "@/modules/admin/update.view";
export const update = createCommand({
data: new SlashCommandBuilder()
.setName("update")
.setDescription("Check for updates and restart the bot")
.addSubcommand(sub =>
sub.setName("check")
.setDescription("Check for and apply available updates")
.addBooleanOption(option =>
option.setName("force")
.setDescription("Force update even if no changes detected")
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName("rollback")
.setDescription("Rollback to the previous version")
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => {
const subcommand = interaction.options.getSubcommand();
if (subcommand === "rollback") {
await handleRollback(interaction);
} else {
await handleUpdate(interaction);
}
}
});
async function handleUpdate(interaction: any) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const force = interaction.options.getBoolean("force") || false;
try {
// 1. Check for updates
await interaction.editReply({ embeds: [getCheckingEmbed()] });
const updateInfo = await UpdateService.checkForUpdates();
if (!updateInfo.hasUpdates && !force) {
await interaction.editReply({
embeds: [getNoUpdatesEmbed(updateInfo.currentCommit)]
});
return;
}
// 2. Analyze requirements
const requirements = await UpdateService.checkUpdateRequirements(updateInfo.branch);
const categories = UpdateService.categorizeChanges(requirements.changedFiles);
// 3. Show confirmation with details
const { embeds, components } = getUpdatesAvailableMessage(
updateInfo,
requirements,
categories,
force
);
const response = await interaction.editReply({ embeds, components });
// 4. Wait for confirmation
try {
const confirmation = await response.awaitMessageComponent({
filter: (i: any) => i.user.id === interaction.user.id,
componentType: ComponentType.Button,
time: 30000
});
if (confirmation.customId === "confirm_update") {
await confirmation.update({
embeds: [getPreparingEmbed()],
components: []
});
// 5. Save rollback point
const previousCommit = await UpdateService.saveRollbackPoint();
// 6. Prepare restart context
await UpdateService.prepareRestartContext({
channelId: interaction.channelId,
userId: interaction.user.id,
timestamp: Date.now(),
runMigrations: requirements.needsMigrations,
installDependencies: requirements.needsRootInstall || requirements.needsWebInstall,
previousCommit: previousCommit.substring(0, 7),
newCommit: updateInfo.latestCommit
});
// 7. Show updating status
await interaction.editReply({
embeds: [getUpdatingEmbed(requirements)]
});
// 8. Perform update
await UpdateService.performUpdate(updateInfo.branch);
// 9. Trigger restart
await UpdateService.triggerRestart();
} else {
await confirmation.update({
embeds: [getCancelledEmbed()],
components: []
});
}
} catch (e) {
if (e instanceof Error && e.message.includes("time")) {
await interaction.editReply({
embeds: [getTimeoutEmbed()],
components: []
});
} else {
throw e;
}
}
} catch (error) {
console.error("Update failed:", error);
await interaction.editReply({
embeds: [getErrorEmbed(error)],
components: []
});
}
}
async function handleRollback(interaction: any) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const hasRollback = await UpdateService.hasRollbackPoint();
if (!hasRollback) {
await interaction.editReply({
embeds: [getRollbackFailedEmbed("No rollback point available. Rollback is only possible after a recent update.")]
});
return;
}
const result = await UpdateService.rollback();
if (result.success) {
await interaction.editReply({
embeds: [getRollbackSuccessEmbed(result.message.split(" ").pop() || "unknown")]
});
// Restart after rollback
setTimeout(() => UpdateService.triggerRestart(), 1000);
} else {
await interaction.editReply({
embeds: [getRollbackFailedEmbed(result.message)]
});
}
} catch (error) {
console.error("Rollback failed:", error);
await interaction.editReply({
embeds: [getErrorEmbed(error)]
});
}
}

View File

@@ -1,12 +1,12 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ModerationService } from "@/modules/moderation/moderation.service";
import { ModerationService } from "@shared/modules/moderation/moderation.service";
import {
getWarnSuccessEmbed,
getModerationErrorEmbed,
getUserWarningEmbed
} from "@/modules/moderation/moderation.view";
import { config } from "@/lib/config";
import { config } from "@shared/lib/config";
export const warn = createCommand({
data: new SlashCommandBuilder()

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ModerationService } from "@/modules/moderation/moderation.service";
import { ModerationService } from "@shared/modules/moderation/moderation.service";
import { getWarningsEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
export const warnings = createCommand({

View File

@@ -1,4 +1,4 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { createErrorEmbed } from "@/lib/embeds";
import { sendWebhookMessage } from "@/lib/webhookUtils";

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { userService } from "@/modules/user/user.service";
import { userService } from "@shared/modules/user/user.service";
import { createBaseEmbed } from "@lib/embeds";
export const balance = createCommand({

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { economyService } from "@/modules/economy/economy.service";
import { economyService } from "@shared/modules/economy/economy.service";
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors";

View File

@@ -1,13 +1,13 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { userService } from "@/modules/user/user.service";
import { userService } from "@shared/modules/user/user.service";
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors";
import { userTimers, users } from "@/db/schema";
import { userTimers, users } from "@db/schema";
import { eq, and, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { config } from "@lib/config";
import { TimerType } from "@/lib/constants";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { config } from "@shared/lib/config";
import { TimerType } from "@shared/lib/constants";
const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM;
const EXAM_TIMER_KEY = 'default';

View File

@@ -1,9 +1,9 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags } from "discord.js";
import { economyService } from "@/modules/economy/economy.service";
import { userService } from "@/modules/user/user.service";
import { config } from "@/lib/config";
import { economyService } from "@shared/modules/economy/economy.service";
import { userService } from "@shared/modules/user/user.service";
import { config } from "@shared/lib/config";
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors";

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags } from "discord.js";
import { tradeService } from "@/modules/trade/trade.service";
import { tradeService } from "@shared/modules/trade/trade.service";
import { getTradeDashboard } from "@/modules/trade/trade.view";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { config } from "@/lib/config";
import { config } from "@shared/lib/config";
import { createErrorEmbed } from "@/lib/embeds";
import { getFeedbackTypeMenu } from "@/modules/feedback/feedback.view";

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { userService } from "@/modules/user/user.service";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { createWarningEmbed } from "@lib/embeds";
import { getInventoryEmbed } from "@/modules/inventory/inventory.view";

View File

@@ -1,12 +1,12 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { userService } from "@/modules/user/user.service";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { createErrorEmbed } from "@lib/embeds";
import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view";
import type { ItemUsageData } from "@/lib/types";
import type { ItemUsageData } from "@shared/lib/types";
import { UserError } from "@/lib/errors";
import { config } from "@/lib/config";
import { config } from "@shared/lib/config";
export const use = createCommand({
data: new SlashCommandBuilder()

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { users, items, inventory } from "@/db/schema";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { users, items, inventory } from "@db/schema";
import { desc, sql, eq } from "drizzle-orm";
import { createWarningEmbed } from "@lib/embeds";
import { getLeaderboardEmbed } from "@/modules/leveling/leveling.view";

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags } from "discord.js";
import { questService } from "@/modules/quest/quest.service";
import { questService } from "@shared/modules/quest/quest.service";
import { createWarningEmbed } from "@lib/embeds";
import { getQuestListEmbed } from "@/modules/quest/quest.view";

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@/lib/utils";
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, AttachmentBuilder } from "discord.js";
import { userService } from "@/modules/user/user.service";
import { userService } from "@shared/modules/user/user.service";
import { generateStudentIdCard } from "@/graphics/studentID";
import { createWarningEmbed } from "@/lib/embeds";

View File

@@ -1,7 +1,7 @@
import { Events } from "discord.js";
import type { Event } from "@lib/types";
import { config } from "@lib/config";
import { userService } from "@modules/user/user.service";
import type { Event } from "@shared/lib/types";
import { config } from "@shared/lib/config";
import { userService } from "@shared/modules/user/user.service";
// Visitor role
const event: Event<Events.GuildMemberAdd> = {

View File

@@ -1,6 +1,6 @@
import { Events } from "discord.js";
import { ComponentInteractionHandler, AutocompleteHandler, CommandHandler } from "@/lib/handlers";
import type { Event } from "@lib/types";
import type { Event } from "@shared/lib/types";
const event: Event<Events.InteractionCreate> = {
name: Events.InteractionCreate,

View File

@@ -1,7 +1,7 @@
import { Events } from "discord.js";
import { userService } from "@/modules/user/user.service";
import { levelingService } from "@/modules/leveling/leveling.service";
import type { Event } from "@lib/types";
import { userService } from "@shared/modules/user/user.service";
import { levelingService } from "@shared/modules/leveling/leveling.service";
import type { Event } from "@shared/lib/types";
const event: Event<Events.MessageCreate> = {
name: Events.MessageCreate,
@@ -15,7 +15,7 @@ const event: Event<Events.MessageCreate> = {
levelingService.processChatXp(message.author.id);
// Activity Tracking for Lootdrops
import("@/modules/economy/lootdrop.service").then(m => m.lootdropService.processMessage(message));
import("@shared/modules/economy/lootdrop.service").then(m => m.lootdropService.processMessage(message));
},
};

View File

@@ -1,6 +1,6 @@
import { Events } from "discord.js";
import { schedulerService } from "@/modules/system/scheduler";
import type { Event } from "@lib/types";
import type { Event } from "@shared/lib/types";
const event: Event<Events.ClientReady> = {
name: Events.ClientReady,
@@ -10,7 +10,7 @@ const event: Event<Events.ClientReady> = {
schedulerService.start();
// Handle post-update tasks
const { UpdateService } = await import("@/modules/admin/update.service");
const { UpdateService } = await import("@shared/modules/admin/update.service");
await UpdateService.handlePostRestart(c);
},
};

View File

@@ -2,12 +2,12 @@ import { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas';
import path from 'path';
// Register Fonts (same as studentID.ts)
const fontDir = path.join(process.cwd(), 'src', 'assets', 'fonts');
const fontDir = path.join(process.cwd(), 'bot', 'assets', 'fonts');
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexSansCondensed-SemiBold.ttf'), 'IBMPlexSansCondensed-SemiBold');
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexMono-Bold.ttf'), 'IBMPlexMono-Bold');
export async function generateLootdropCard(amount: number, currency: string): Promise<Buffer> {
const templatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'lootdrop', 'template.png');
const templatePath = path.join(process.cwd(), 'bot', 'assets', 'graphics', 'lootdrop', 'template.png');
const template = await loadImage(templatePath);
const canvas = createCanvas(template.width, template.height);
@@ -50,7 +50,7 @@ export async function generateLootdropCard(amount: number, currency: string): Pr
}
export async function generateClaimedLootdropCard(amount: number, currency: string, username: string, avatarUrl: string): Promise<Buffer> {
const templatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'lootdrop', 'template.png');
const templatePath = path.join(process.cwd(), 'bot', 'assets', 'graphics', 'lootdrop', 'template.png');
const template = await loadImage(templatePath);
const canvas = createCanvas(template.width, template.height);

View File

@@ -1,9 +1,9 @@
import { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas';
import { levelingService } from '@/modules/leveling/leveling.service';
import { levelingService } from '@shared/modules/leveling/leveling.service';
import path from 'path';
// Register Fonts
const fontDir = path.join(process.cwd(), 'src', 'assets', 'fonts');
const fontDir = path.join(process.cwd(), 'bot', 'assets', 'fonts');
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexSansCondensed-SemiBold.ttf'), 'IBMPlexSansCondensed-SemiBold');
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexMono-Bold.ttf'), 'IBMPlexMono-Bold');
@@ -18,8 +18,8 @@ interface StudentCardData {
}
export async function generateStudentIdCard(data: StudentCardData): Promise<Buffer> {
const templatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'studentID', 'template.png');
const classTemplatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'studentID', `Constellation-${data.className}.png`);
const templatePath = path.join(process.cwd(), 'bot', 'assets', 'graphics', 'studentID', 'template.png');
const classTemplatePath = path.join(process.cwd(), 'bot', 'assets', 'graphics', 'studentID', `Constellation-${data.className}.png`);
const template = await loadImage(templatePath);
const classTemplate = await loadImage(classTemplatePath);

48
bot/index.ts Normal file
View File

@@ -0,0 +1,48 @@
import { AuroraClient } from "@/lib/BotClient";
import { env } from "@shared/lib/env";
import { join } from "node:path";
import { startWebServerFromRoot } from "../web/src/server";
// Load commands & events
await AuroraClient.loadCommands();
await AuroraClient.loadEvents();
await AuroraClient.deployCommands();
console.log("🌐 Starting web server...");
let shuttingDown = false;
const webProjectPath = join(import.meta.dir, "../web");
const webPort = Number(process.env.WEB_PORT) || 3000;
const webHost = process.env.HOST || "0.0.0.0";
// Start web server in the same process
const webServer = await startWebServerFromRoot(webProjectPath, {
port: webPort,
hostname: webHost,
});
// login with the token from .env
if (!env.DISCORD_BOT_TOKEN) {
throw new Error("❌ DISCORD_BOT_TOKEN is not set in environment variables.");
}
AuroraClient.login(env.DISCORD_BOT_TOKEN);
// Handle graceful shutdown
const shutdownHandler = async () => {
if (shuttingDown) return;
shuttingDown = true;
console.log("🛑 Shutdown signal received. Stopping services...");
// Stop web server
await webServer.stop();
// Stop bot
AuroraClient.shutdown();
process.exit(0);
};
process.on("SIGINT", shutdownHandler);
process.on("SIGTERM", shutdownHandler);

View File

@@ -1,7 +1,7 @@
import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js";
import { join } from "node:path";
import type { Command } from "@lib/types";
import { env } from "@lib/env";
import type { Command } from "@shared/lib/types";
import { env } from "@shared/lib/env";
import { CommandLoader } from "@lib/loaders/CommandLoader";
import { EventLoader } from "@lib/loaders/EventLoader";
@@ -96,7 +96,7 @@ export class Client extends DiscordClient {
async shutdown() {
const { setShuttingDown, waitForTransactions } = await import("./shutdown");
const { closeDatabase } = await import("./DrizzleClient");
const { closeDatabase } = await import("@shared/db/DrizzleClient");
console.log("🛑 Shutdown signal received. Starting graceful shutdown...");
setShuttingDown(true);

View File

@@ -1,5 +1,5 @@
import { DrizzleClient } from "./DrizzleClient";
import type { Transaction } from "./types";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import type { Transaction } from "@shared/lib/types";
import { isShuttingDown, incrementTransactions, decrementTransactions } from "./shutdown";
export const withTransaction = async <T>(

View File

@@ -4,7 +4,7 @@ import { AuroraClient } from "@/lib/BotClient";
import { ChatInputCommandInteraction } from "discord.js";
// Mock UserService
mock.module("@/modules/user/user.service", () => ({
mock.module("@shared/modules/user/user.service", () => ({
userService: {
getOrCreateUser: mock(() => Promise.resolve())
}

View File

@@ -1,6 +1,6 @@
import { ChatInputCommandInteraction, MessageFlags } from "discord.js";
import { AuroraClient } from "@/lib/BotClient";
import { userService } from "@/modules/user/user.service";
import { userService } from "@shared/modules/user/user.service";
import { createErrorEmbed } from "@lib/embeds";

View File

@@ -1,7 +1,7 @@
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import type { Command } from "@lib/types";
import { config } from "@lib/config";
import type { Command } from "@shared/lib/types";
import { config } from "@shared/lib/config";
import type { LoadResult, LoadError } from "./types";
import type { Client } from "../BotClient";

View File

@@ -1,6 +1,6 @@
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import type { Event } from "@lib/types";
import type { Event } from "@shared/lib/types";
import type { LoadResult } from "./types";
import type { Client } from "../BotClient";

View File

@@ -6,13 +6,13 @@ import { ButtonInteraction, ModalSubmitInteraction, StringSelectMenuInteraction
const valuesMock = mock((_args: any) => Promise.resolve());
const insertMock = mock(() => ({ values: valuesMock }));
mock.module("@/lib/DrizzleClient", () => ({
mock.module("@shared/db/DrizzleClient", () => ({
DrizzleClient: {
insert: insertMock
}
}));
mock.module("@/db/schema", () => ({
mock.module("@db/schema", () => ({
items: "items_schema"
}));

View File

@@ -1,10 +1,10 @@
import { type Interaction } from "discord.js";
import { items } from "@/db/schema";
import { DrizzleClient } from "@/lib/DrizzleClient";
import type { ItemUsageData, ItemEffect } from "@/lib/types";
import { items } from "@db/schema";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import type { ItemUsageData, ItemEffect } from "@shared/lib/types";
import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view";
import type { DraftItem } from "./item_wizard.types";
import { ItemType, EffectType } from "@/lib/constants";
import { ItemType, EffectType } from "@shared/lib/constants";
// --- Types ---

View File

@@ -1,4 +1,4 @@
import type { ItemUsageData } from "@/lib/types";
import type { ItemUsageData } from "@shared/lib/types";
export interface DraftItem {
name: string;

View File

@@ -10,7 +10,7 @@ import {
} from "discord.js";
import { createBaseEmbed } from "@lib/embeds";
import type { DraftItem } from "./item_wizard.types";
import { ItemType } from "@/lib/constants";
import { ItemType } from "@shared/lib/constants";
const getItemTypeOptions = () => [
{ label: "Material", value: ItemType.MATERIAL, description: "Used for crafting or trading" },

View File

@@ -0,0 +1,33 @@
export interface RestartContext {
channelId: string;
userId: string;
timestamp: number;
runMigrations: boolean;
installDependencies: boolean;
previousCommit: string;
newCommit: string;
}
export interface UpdateCheckResult {
needsRootInstall: boolean;
needsWebInstall: boolean;
needsMigrations: boolean;
changedFiles: string[];
error?: Error;
}
export interface UpdateInfo {
hasUpdates: boolean;
branch: string;
currentCommit: string;
latestCommit: string;
commitCount: number;
commits: CommitInfo[];
}
export interface CommitInfo {
hash: string;
message: string;
author: string;
}

View File

@@ -0,0 +1,274 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { createInfoEmbed, createSuccessEmbed, createWarningEmbed, createErrorEmbed } from "@lib/embeds";
import type { UpdateInfo, UpdateCheckResult } from "./update.types";
// Constants for UI
const LOG_TRUNCATE_LENGTH = 800;
const OUTPUT_TRUNCATE_LENGTH = 400;
function truncate(text: string, maxLength: number): string {
if (!text) return "";
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
}
// ============ Pre-Update Embeds ============
export function getCheckingEmbed() {
return createInfoEmbed("🔍 Fetching latest changes from remote...", "Checking for Updates");
}
export function getNoUpdatesEmbed(currentCommit: string) {
return createSuccessEmbed(
`You're running the latest version.\n\n**Current:** \`${currentCommit}\``,
"✅ Already Up to Date"
);
}
export function getUpdatesAvailableMessage(
updateInfo: UpdateInfo,
requirements: UpdateCheckResult,
changeCategories: Record<string, number>,
force: boolean
) {
const { branch, currentCommit, latestCommit, commitCount, commits } = updateInfo;
const { needsRootInstall, needsWebInstall, needsMigrations } = requirements;
// Build commit list (max 5)
const commitList = commits
.slice(0, 5)
.map(c => `\`${c.hash}\` ${truncate(c.message, 50)}`)
.join("\n");
const moreCommits = commitCount > 5 ? `\n*...and ${commitCount - 5} more*` : "";
// Build change categories
const categoryList = Object.entries(changeCategories)
.map(([cat, count]) => `${cat}: ${count} file${count > 1 ? "s" : ""}`)
.join("\n");
// Build requirements list
const reqs: string[] = [];
if (needsRootInstall) reqs.push("📦 Install root dependencies");
if (needsWebInstall) reqs.push("🌐 Install web dependencies");
if (needsMigrations) reqs.push("🗃️ Run database migrations");
if (reqs.length === 0) reqs.push("⚡ Quick update (no extra steps)");
const embed = new EmbedBuilder()
.setTitle("📥 Updates Available")
.setColor(force ? 0xFF6B6B : 0x5865F2)
.addFields(
{
name: "Version",
value: `\`${currentCommit}\`\`${latestCommit}\``,
inline: true
},
{
name: "Branch",
value: `\`${branch}\``,
inline: true
},
{
name: "Commits",
value: `${commitCount} new commit${commitCount > 1 ? "s" : ""}`,
inline: true
},
{
name: "Recent Changes",
value: commitList + moreCommits || "No commits",
inline: false
},
{
name: "Files Changed",
value: categoryList || "Unknown",
inline: true
},
{
name: "Update Actions",
value: reqs.join("\n"),
inline: true
}
)
.setFooter({ text: force ? "⚠️ Force mode enabled" : "This will restart the bot" })
.setTimestamp();
const confirmButton = new ButtonBuilder()
.setCustomId("confirm_update")
.setLabel(force ? "Force Update" : "Update Now")
.setEmoji(force ? "⚠️" : "🚀")
.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] };
}
// ============ Update Progress Embeds ============
export function getPreparingEmbed() {
return createInfoEmbed(
"🔒 Saving rollback point...\n📥 Preparing to download updates...",
"⏳ Preparing Update"
);
}
export function getUpdatingEmbed(requirements: UpdateCheckResult) {
const steps: string[] = ["✅ Rollback point saved"];
steps.push("📥 Downloading updates...");
if (requirements.needsRootInstall || requirements.needsWebInstall) {
steps.push("📦 Dependencies will be installed after restart");
}
if (requirements.needsMigrations) {
steps.push("🗃️ Migrations will run after restart");
}
steps.push("\n🔄 **Restarting now...**");
return createWarningEmbed(steps.join("\n"), "🚀 Updating");
}
export function getCancelledEmbed() {
return createInfoEmbed("Update cancelled. No changes were made.", "❌ Cancelled");
}
export function getTimeoutEmbed() {
return createWarningEmbed(
"No response received within 30 seconds.\nRun `/update` again when ready.",
"⏰ Timed Out"
);
}
export function getErrorEmbed(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return createErrorEmbed(
`The update could not be completed:\n\`\`\`\n${truncate(message, 500)}\n\`\`\``,
"❌ Update Failed"
);
}
// ============ Post-Restart Embeds ============
export interface PostRestartResult {
installSuccess: boolean;
installOutput: string;
migrationSuccess: boolean;
migrationOutput: string;
ranInstall: boolean;
ranMigrations: boolean;
previousCommit?: string;
newCommit?: string;
}
export function getPostRestartEmbed(result: PostRestartResult, hasRollback: boolean) {
const isSuccess = result.installSuccess && result.migrationSuccess;
const embed = new EmbedBuilder()
.setTitle(isSuccess ? "✅ Update Complete" : "⚠️ Update Completed with Issues")
.setColor(isSuccess ? 0x57F287 : 0xFEE75C)
.setTimestamp();
// Version info
if (result.previousCommit && result.newCommit) {
embed.addFields({
name: "Version",
value: `\`${result.previousCommit}\`\`${result.newCommit}\``,
inline: false
});
}
// Results summary
const results: string[] = [];
if (result.ranInstall) {
results.push(result.installSuccess
? "✅ Dependencies installed"
: "❌ Dependency installation failed"
);
}
if (result.ranMigrations) {
results.push(result.migrationSuccess
? "✅ Migrations applied"
: "❌ Migration failed"
);
}
if (results.length > 0) {
embed.addFields({
name: "Actions Performed",
value: results.join("\n"),
inline: false
});
}
// Output details (collapsed if too long)
if (result.installOutput && !result.installSuccess) {
embed.addFields({
name: "Install Output",
value: `\`\`\`\n${truncate(result.installOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``,
inline: false
});
}
if (result.migrationOutput && !result.migrationSuccess) {
embed.addFields({
name: "Migration Output",
value: `\`\`\`\n${truncate(result.migrationOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``,
inline: false
});
}
// Footer with rollback hint
if (!isSuccess && hasRollback) {
embed.setFooter({ text: "💡 Use /update rollback to revert if needed" });
}
// Build components
const components: ActionRowBuilder<ButtonBuilder>[] = [];
if (!isSuccess && hasRollback) {
const rollbackButton = new ButtonBuilder()
.setCustomId("rollback_update")
.setLabel("Rollback")
.setEmoji("↩️")
.setStyle(ButtonStyle.Danger);
components.push(new ActionRowBuilder<ButtonBuilder>().addComponents(rollbackButton));
}
return { embeds: [embed], components };
}
export function getInstallingDependenciesEmbed() {
return createInfoEmbed(
"📦 Installing dependencies for root and web projects...\nThis may take a moment.",
"⏳ Installing Dependencies"
);
}
export function getRunningMigrationsEmbed() {
return createInfoEmbed(
"🗃️ Applying database migrations...",
"⏳ Running Migrations"
);
}
export function getRollbackSuccessEmbed(commit: string) {
return createSuccessEmbed(
`Successfully rolled back to commit \`${commit}\`.\nThe bot will restart now.`,
"↩️ Rollback Complete"
);
}
export function getRollbackFailedEmbed(error: string) {
return createErrorEmbed(
`Could not rollback:\n\`\`\`\n${error}\n\`\`\``,
"❌ Rollback Failed"
);
}

View File

@@ -1,5 +1,5 @@
import { ButtonInteraction } from "discord.js";
import { lootdropService } from "./lootdrop.service";
import { lootdropService } from "@shared/modules/economy/lootdrop.service";
import { UserError } from "@/lib/errors";
import { getLootdropClaimedMessage } from "./lootdrop.view";

View File

@@ -1,6 +1,6 @@
import { ButtonInteraction, MessageFlags } from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { userService } from "@/modules/user/user.service";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { UserError } from "@/lib/errors";
export async function handleShopInteraction(interaction: ButtonInteraction) {

View File

@@ -1,6 +1,6 @@
import type { Interaction } from "discord.js";
import { TextChannel, MessageFlags } from "discord.js";
import { config } from "@/lib/config";
import { config } from "@shared/lib/config";
import { AuroraClient } from "@/lib/BotClient";
import { buildFeedbackMessage, getFeedbackModal } from "./feedback.view";
import { FEEDBACK_CUSTOM_IDS, type FeedbackType, type FeedbackData } from "./feedback.types";

View File

@@ -1,11 +1,11 @@
import { levelingService } from "@/modules/leveling/leveling.service";
import { economyService } from "@/modules/economy/economy.service";
import { userTimers } from "@/db/schema";
import { levelingService } from "@shared/modules/leveling/leveling.service";
import { economyService } from "@shared/modules/economy/economy.service";
import { userTimers } from "@db/schema";
import type { EffectHandler } from "./types";
import type { LootTableItem } from "@/lib/types";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { inventory, items } from "@/db/schema";
import { TimerType, TransactionType, LootType } from "@/lib/constants";
import type { LootTableItem } from "@shared/lib/types";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { inventory, items } from "@db/schema";
import { TimerType, TransactionType, LootType } from "@shared/lib/constants";
// Helper to extract duration in seconds
@@ -120,7 +120,7 @@ export const handleLootbox: EffectHandler = async (userId, effect, txFn) => {
// Try to fetch item name for the message
try {
const item = await txFn.query.items.findFirst({
where: (items, { eq }) => eq(items.id, winner.itemId!)
where: (items: any, { eq }: any) => eq(items.id, winner.itemId!)
});
if (item) {
return winner.message || `You found ${quantity > 1 ? quantity + 'x ' : ''}**${item.name}**!`;

View File

@@ -1,4 +1,4 @@
import type { Transaction } from "@/lib/types";
import type { Transaction } from "@shared/lib/types";
export type EffectHandler = (userId: string, effect: any, txFn: Transaction) => Promise<string>;

View File

@@ -1,6 +1,6 @@
import { EmbedBuilder } from "discord.js";
import type { ItemUsageData } from "@/lib/types";
import { EffectType } from "@/lib/constants";
import type { ItemUsageData } from "@shared/lib/types";
import { EffectType } from "@shared/lib/constants";
/**
* Inventory entry with item details

View File

@@ -1,4 +1,4 @@
import { CaseType } from "@/lib/constants";
import { CaseType } from "@shared/lib/constants";
export { CaseType };

View File

@@ -1,4 +1,4 @@
import { temporaryRoleService } from "./temp-role.service";
import { temporaryRoleService } from "@shared/modules/system/temp-role.service";
export const schedulerService = {
start: () => {
@@ -10,7 +10,7 @@ export const schedulerService = {
}, 60 * 1000);
// 2. Terminal Update Loop (every 60s)
const { terminalService } = require("@/modules/terminal/terminal.service");
const { terminalService } = require("@shared/modules/terminal/terminal.service");
setInterval(() => {
terminalService.update();
}, 60 * 1000);

View File

@@ -7,8 +7,8 @@ import {
TextChannel,
EmbedBuilder
} from "discord.js";
import { tradeService } from "./trade.service";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { tradeService } from "@shared/modules/trade/trade.service";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds";
import { UserError } from "@lib/errors";
import { getTradeDashboard, getTradeMoneyModal, getItemSelectMenu, getTradeCompletedEmbed } from "./trade.view";
@@ -101,7 +101,7 @@ async function handleAddItemClick(interaction: ButtonInteraction, threadId: stri
}
// Slice top 25 for select menu
const options = inventory.slice(0, 25).map(entry => ({
const options = inventory.slice(0, 25).map((entry: any) => ({
label: `${entry.item.name} (${entry.quantity})`,
value: entry.item.id.toString(),
description: `Rarity: ${entry.item.rarity} `

View File

@@ -1,8 +1,8 @@
import { ButtonInteraction, MessageFlags } from "discord.js";
import { config } from "@/lib/config";
import { config } from "@shared/lib/config";
import { getEnrollmentSuccessMessage } from "./enrollment.view";
import { classService } from "@modules/class/class.service";
import { userService } from "@modules/user/user.service";
import { classService } from "@shared/modules/class/class.service";
import { userService } from "@shared/modules/user/user.service";
import { UserError } from "@/lib/errors";
import { sendWebhookMessage } from "@/lib/webhookUtils";
@@ -37,7 +37,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
// 2. Get available classes
const allClasses = await classService.getAllClasses();
const validClasses = allClasses.filter(c => c.roleId);
const validClasses = allClasses.filter((c: any) => c.roleId);
if (validClasses.length === 0) {
throw new UserError("No classes with specified roles found in database.");

View File

@@ -1,7 +1,7 @@
import { userTimers } from "@/db/schema";
import { userTimers } from "@db/schema";
import { eq, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { TimerType } from "@/lib/constants";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { TimerType } from "@shared/lib/constants";
export { TimerType };
@@ -33,7 +33,7 @@ export const userTimerService = {
if (tx) {
return await execute(tx);
} else {
return await DrizzleClient.transaction(async (t) => {
return await DrizzleClient.transaction(async (t: any) => {
return await execute(t);
});
}
@@ -89,7 +89,7 @@ export const userTimerService = {
if (tx) {
return await execute(tx);
} else {
return await DrizzleClient.transaction(async (t) => {
return await DrizzleClient.transaction(async (t: any) => {
return await execute(t);
});
}

View File

@@ -6,11 +6,20 @@ services:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
ports:
- "127.0.0.1:${DB_PORT}:5432"
# Uncomment to access DB from host (for debugging/drizzle-kit studio)
# ports:
# - "127.0.0.1:${DB_PORT}:5432"
volumes:
- ./src/db/data:/var/lib/postgresql/data
- ./src/db/log:/var/log/postgresql
- ./shared/db/data:/var/lib/postgresql/data
- ./shared/db/log:/var/log/postgresql
networks:
- internal
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}" ]
interval: 5s
timeout: 5s
retries: 5
app:
container_name: aurora_app
restart: unless-stopped
@@ -24,19 +33,30 @@ services:
volumes:
- .:/app
- /app/node_modules
- /app/web/node_modules
environment:
- HOST=0.0.0.0
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME}
- DB_PORT=${DB_PORT}
- DB_PORT=5432
- DB_HOST=db
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
depends_on:
- db
db:
condition: service_healthy
networks:
- internal
- web
healthcheck:
test: [ "CMD", "bun", "-e", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
command: bun run dev
studio:
@@ -51,13 +71,24 @@ services:
volumes:
- .:/app
- /app/node_modules
- /app/web/node_modules
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME}
- DB_PORT=${DB_PORT}
- DB_PORT=5432
- DB_HOST=db
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
depends_on:
- db
db:
condition: service_healthy
networks:
- internal
command: bun run db:studio
networks:
internal:
driver: bridge
internal: true # No external access
web:
driver: bridge # Can be accessed from host

View File

@@ -1,5 +1,5 @@
import { defineConfig } from "drizzle-kit";
import { env } from "./src/lib/env";
import { env } from "./shared/lib/env";
// @ts-expect-error - Polyfill for BigInt serialization
BigInt.prototype.toJSON = function () {
@@ -7,8 +7,8 @@ BigInt.prototype.toJSON = function () {
};
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
schema: "./shared/db/schema.ts",
out: "./shared/db/migrations",
dialect: "postgresql",
dbCredentials: {
url: env.DATABASE_URL,

View File

@@ -1,6 +1,6 @@
{
"name": "app",
"module": "index.ts",
"module": "bot/index.ts",
"type": "module",
"private": true,
"devDependencies": {
@@ -15,9 +15,11 @@
"migrate": "docker compose run --rm app drizzle-kit migrate",
"db:push": "docker compose run --rm app drizzle-kit push",
"db:push:local": "drizzle-kit push",
"dev": "bun --watch src/index.ts",
"dev": "bun --watch bot/index.ts",
"db:studio": "drizzle-kit studio --host 0.0.0.0",
"studio:remote": "bash scripts/remote-studio.sh",
"studio:remote": "bash shared/scripts/remote-studio.sh",
"dashboard:remote": "bash shared/scripts/remote-dashboard.sh",
"remote": "bash shared/scripts/remote.sh",
"test": "bun test"
},
"dependencies": {

View File

@@ -1,7 +1,7 @@
import { drizzle } from "drizzle-orm/bun-sql";
import { SQL } from "bun";
import * as schema from "@db/schema";
import { env } from "@lib/env";
import * as schema from "./schema";
import { env } from "@shared/lib/env";
const connectionString = env.DATABASE_URL;
export const postgres = new SQL(connectionString);

View File

@@ -1,5 +1,5 @@
import { expect, test, describe } from "bun:test";
import { postgres } from "../lib/DrizzleClient";
import { postgres } from "./DrizzleClient";
describe("Database Indexes", () => {
test("should have indexes on users table", async () => {

Some files were not shown because too many files have changed in this diff Show More