Merge branch 'main' into codex/housekeeping-foundation
CI / check (pull_request) Successful in 28s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-26 19:50:26 +02:00
commit 2a36ba1956
68 files changed
+1073 -528

No files matched your search

+47 -63
View File
@@ -1,4 +1,4 @@
import { and, asc, count, desc, eq, gt, inArray, or } from "drizzle-orm";
import { and, asc, count, desc, eq, gt, or } from "drizzle-orm";
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
@@ -17,7 +17,6 @@ import {
UserReferrals,
UsersBadges,
UsersSettings,
WebsiteLoginLogs,
} from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { resolveHotelName } from "@/lib/hotel-name";
@@ -58,10 +57,7 @@ const ERROR_MESSAGES: Record<string, string> = {
};
function getErrorMessage(error: string): string {
if (error === "not_enough") return ERROR_MESSAGES.not_enough;
if (error === "no_referrals") return ERROR_MESSAGES.no_referrals;
if (error === "bad_config") return ERROR_MESSAGES.bad_config;
return ERROR_MESSAGES.error;
return ERROR_MESSAGES[error] ?? ERROR_MESSAGES.error;
}
export default async function MePage({
@@ -90,10 +86,11 @@ export default async function MePage({
alertRaw,
recentRooms,
badges,
lastWebLoginRows,
userSettingsRows,
friendCountRows,
unreadCountRows,
referralsRows,
friends,
] = await Promise.all([
db
.select({
@@ -139,13 +136,6 @@ export default async function MePage({
.where(and(eq(UsersBadges.userId, userId), gt(UsersBadges.slotId, 0)))
.orderBy(asc(UsersBadges.slotId))
.catch(() => []),
db
.select({ createdAt: WebsiteLoginLogs.createdAt })
.from(WebsiteLoginLogs)
.where(eq(WebsiteLoginLogs.userId, userId))
.orderBy(desc(WebsiteLoginLogs.id))
.limit(1)
.catch(() => []),
db
.select({
achievementScore: UsersSettings.achievementScore,
@@ -170,18 +160,51 @@ export default async function MePage({
.from(MessengerOffline)
.where(eq(MessengerOffline.userId, userId))
.catch(() => [{ value: 0 }]),
db
.select({ referralsTotal: UserReferrals.referralsTotal })
.from(UserReferrals)
.where(eq(UserReferrals.userId, userId))
.orderBy(desc(UserReferrals.id))
.limit(1)
.catch(() => []),
db
.select({
id: User.id,
username: User.username,
look: User.look,
motto: User.motto,
online: User.online,
})
.from(MessengerFriendships)
.innerJoin(
User,
or(
and(
eq(MessengerFriendships.userOneId, userId),
eq(User.id, MessengerFriendships.userTwoId),
),
and(
eq(MessengerFriendships.userTwoId, userId),
eq(User.id, MessengerFriendships.userOneId),
),
),
)
.where(
or(
eq(MessengerFriendships.userOneId, userId),
eq(MessengerFriendships.userTwoId, userId),
),
)
.catch(() => []),
]);
const user = userRows[0] ?? null;
const lastWebLogin = lastWebLoginRows[0] ?? null;
if (!user) throw new Error("user-not-found");
const userSettings = userSettingsRows[0] ?? null;
const friendCount = Number(friendCountRows[0]?.value ?? 0);
const unreadCount = Number(unreadCountRows[0]?.value ?? 0);
void lastWebLogin;
if (!user) throw new Error("user-not-found");
const needed = Number.parseInt(neededRaw ?? "5", 10) || 5;
const rewardAmount = Number.parseInt(rewardAmountRaw ?? "30", 10) || 0;
const rewardCurrency = (
@@ -192,53 +215,12 @@ export default async function MePage({
.trim()
.toLowerCase();
const [referrals] = await db
.select({ referralsTotal: UserReferrals.referralsTotal })
.from(UserReferrals)
.where(eq(UserReferrals.userId, userId))
.orderBy(desc(UserReferrals.id))
.limit(1)
.catch(() => []);
const referralTotal = referrals ? Number(referrals.referralsTotal) : 0;
const referralTotal = referralsRows[0]
? Number(referralsRows[0].referralsTotal)
: 0;
const canClaim = referralTotal >= needed;
const remaining = Math.max(0, needed - referralTotal);
const friendships = await db
.select({
userOneId: MessengerFriendships.userOneId,
userTwoId: MessengerFriendships.userTwoId,
})
.from(MessengerFriendships)
.where(
or(
eq(MessengerFriendships.userOneId, userId),
eq(MessengerFriendships.userTwoId, userId),
),
)
.catch(() => []);
const friendIds = Array.from(
new Set(
friendships
.map((f) => (f.userOneId === userId ? f.userTwoId : f.userOneId))
.filter((id) => id && id !== userId),
),
);
const friends = friendIds.length
? await db
.select({
id: User.id,
username: User.username,
look: User.look,
motto: User.motto,
online: User.online,
})
.from(User)
.where(inArray(User.id, friendIds))
.catch(() => [])
: [];
const onlineFriends = friends.filter((f) => f.online === "1");
const registered = new Date(user.accountCreated * 1000)
@@ -255,6 +237,7 @@ export default async function MePage({
const avatarFull = avatarImageUrl(user.look, {
direction: 2,
});
content = (
<div className="flex flex-col gap-8 pb-8">
{claimed ? (
@@ -298,6 +281,7 @@ export default async function MePage({
/>
<Link
href="/client"
prefetch={false}
className="btn-shine flex items-center justify-center gap-1.5 rounded-xl border py-2 px-4 font-extrabold text-xs transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg"
style={{
background: "var(--color-primary)",
+14 -12
View File
@@ -33,18 +33,20 @@ export default async function RegisterPage() {
.where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0),
db
.select({ username: User.username, look: User.look })
.from(User)
.where(eq(User.online, "1"))
.limit(8)
.catch(() => []),
db
.select({ username: User.username, look: User.look })
.from(User)
.orderBy(desc(User.accountCreated))
.limit(8)
.catch(() => []),
cached("register_online_users", 10_000, () =>
db
.select({ username: User.username, look: User.look })
.from(User)
.where(eq(User.online, "1"))
.limit(8),
).catch(() => []),
cached("register_latest_users", 30_000, () =>
db
.select({ username: User.username, look: User.look })
.from(User)
.orderBy(desc(User.accountCreated))
.limit(8),
).catch(() => []),
]);
return (
+12 -3
View File
@@ -34,7 +34,13 @@ function ToolbarBtn({
} as const;
if (href) {
return (
<Link href={href} className={cls} style={style} title={title}>
<Link
href={href}
prefetch={false}
className={cls}
style={style}
title={title}
>
{children}
</Link>
);
@@ -377,9 +383,12 @@ export function ClientView({
<iframe
id="nitro"
src={clientSrc}
className="absolute inset-0 w-full h-full border-0 m-0 p-0 overflow-hidden"
className={`absolute inset-0 w-full h-full border-0 m-0 p-0 overflow-hidden ${
dragging ? "pointer-events-none" : "pointer-events-auto"
}`}
title={hotelName}
allow="autoplay; gamepad"
allow="autoplay; gamepad; fullscreen"
loading="eager"
/>
</div>
</>
+12 -8
View File
@@ -21,15 +21,19 @@ export default async function ClientPage() {
]);
const ip = (await headers()).get("x-real-client-ip") ?? "0.0.0.0";
const ticket = await issueSsoTicket(userId, hotelName, ip);
const onlineCount = await cached("online_count", 10_000, async () => {
const [row] = await db
.select({ total: count() })
.from(User)
.where(eq(User.online, "1"));
return row?.total ?? 0;
}).catch(() => 0);
// Ticket write and online count run in parallel — the client page should
// render as fast as possible since the player is waiting for the game.
const [ticket, onlineCount] = await Promise.all([
issueSsoTicket(userId, hotelName, ip),
cached("online_count", 10_000, async () => {
const [row] = await db
.select({ total: count() })
.from(User)
.where(eq(User.online, "1"));
return row?.total ?? 0;
}).catch(() => 0),
]);
return (
<ClientView
+1 -1
View File
@@ -33,7 +33,7 @@ const pixelify = Pixelify_Sans({
// enforceSiteAccess, DB-backed site settings) to render the <html> element,
// which cannot be wrapped in <Suspense>. Child pages inherit this opt-out,
// so leaf pages that only access cached/DB data stay instant.
export const instant = false;
// export const instant = false;
export async function generateMetadata(): Promise<Metadata> {
const appUrl = process.env.APP_URL ?? "http://localhost:3000";