refactor: split monolith into 3 microservices (frontend, backend, discord-gateway)

- Extract services into services/{frontend,backend,discord-gateway}
- Create packages/shared/ for shared logger, errors, utils, types
- Setup Modular MVC pattern in backend (controller→service→repository)
- Setup event-driven architecture in discord-gateway with Redis pub/sub
- Move Docker files to infra/docker/ with per-service Dockerfiles
- Update docker-compose.yml to use Traefik-only routing (no port exposes)
- Update GitHub Actions deploy workflow for multi-service matrix build
- Fix all import paths and resolve type errors across all services
- All 3 services pass tsc --noEmit clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-01 21:44:29 +07:00
co-authored by Claude Opus 4.8
parent bda8304bb9
commit c48a0c5e3b
193 changed files with 16879 additions and 1158 deletions
@@ -0,0 +1,136 @@
import type { MessageRecord } from "../../../shared/api/client";
interface MessageMetadata {
stickers?: Array<{ name?: string; url?: string }>;
attachments?: Array<{ name: string; url: string; contentType?: string }>;
embeds?: Array<{ title?: string; image?: string; thumbnail?: string }>;
}
interface ImageItem {
url: string;
title: string;
kind: "attachment" | "embed" | "sticker";
message: MessageRecord;
}
function parseMetadata(value: string | null): MessageMetadata {
if (!value) return {};
try {
return JSON.parse(value) as MessageMetadata;
} catch {
return {};
}
}
export function ImageGrid({ messages }: { messages: MessageRecord[] }) {
const images: ImageItem[] = [];
for (const message of messages) {
const metadata = parseMetadata(message.metadata);
// Stickers
for (const sticker of metadata.stickers ?? []) {
if (sticker.url) {
images.push({
url: sticker.url,
title: sticker.name || "sticker",
kind: "sticker",
message,
});
}
}
// Attachments
for (const attachment of metadata.attachments ?? []) {
if (
attachment.url &&
(attachment.contentType?.startsWith("image/") ||
/\.(png|jpe?g|gif|webp)$/i.test(attachment.name))
) {
images.push({
url: attachment.url,
title: attachment.name,
kind: "attachment",
message,
});
}
}
// Embed images
for (const embed of metadata.embeds ?? []) {
for (const imgUrl of [embed.image, embed.thumbnail].filter(Boolean)) {
images.push({
url: imgUrl as string,
title: embed.title || "embed image",
kind: "embed",
message,
});
}
}
}
if (images.length === 0) {
return (
<div className="rounded-2xl border border-dashed border-border p-10 text-center text-sm text-muted-foreground">
No images found.
</div>
);
}
return (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{images.map((image, index) => {
// Stable key using message.id + url
const stableKey = `${image.message.id}-${image.kind}-${index}`;
return (
<a
key={stableKey}
href={image.url}
target="_blank"
rel="noreferrer"
className="group overflow-hidden rounded-2xl border border-border bg-card shadow-sm transition-all hover:border-primary/30 hover:shadow-md"
>
<div className="relative aspect-video overflow-hidden">
{image.kind === "sticker" ? (
<img
src={image.url}
alt={image.title}
className="h-full w-full object-contain bg-muted/30 p-2 transition-transform group-hover:scale-105"
loading="lazy"
/>
) : (
<img
src={image.url}
alt={image.title}
className="h-full w-full object-cover transition-transform group-hover:scale-105"
loading="lazy"
/>
)}
<div className="absolute right-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-white backdrop-blur">
{image.kind}
</div>
</div>
<div className="p-3">
<div className="truncate text-sm font-medium">{image.title}</div>
<div className="flex items-center gap-2">
<div className="h-4 w-4 overflow-hidden rounded-full">
<img
src={
image.message.avatar_url ??
"https://cdn.discordapp.com/embed/avatars/0.png"
}
alt=""
className="h-full w-full object-cover"
/>
</div>
<span className="truncate text-xs text-muted-foreground">
{image.message.username}
</span>
</div>
</div>
</a>
);
})}
</div>
);
}