feat(docker): prepare portable images with runtime hotel configuration
CI / check (push) Successful in 1m6s
CI / deploy (push) Successful in 1m23s

This commit is contained in:
Simo committed 2026-09-07 22:37:53 +02:00
1 parent 745d0b7247
commit c4496e710b
28 files changed
+622 -104

No files matched your search

@@ -61,11 +61,7 @@ export function UserBadgesSection({ userId, badges }: UserBadgesSectionProps) {
className="group flex items-center gap-1.5 rounded-md border bg-card px-2 py-1 text-sm hover:border-destructive/50 transition-colors"
>
<img
src={
process.env.NEXT_PUBLIC_BADGE_URL
? `${process.env.NEXT_PUBLIC_BADGE_URL}/${badge.badgeCode}.gif`
: `/swf/c_images/album1584/${badge.badgeCode}.gif`
}
src={`/api/imaging/badge?source=local&code=${encodeURIComponent(badge.badgeCode)}`}
alt={badge.badgeCode}
className="h-5 w-5"
onError={(e) => {
@@ -18,11 +18,7 @@ export function UserBadgesReadonly({ badges }: { badges: Badge[] }) {
className="flex items-center gap-1.5 rounded-md border bg-card px-2 py-1 text-sm"
>
<img
src={
process.env.NEXT_PUBLIC_BADGE_URL
? `${process.env.NEXT_PUBLIC_BADGE_URL}/${badge.badgeCode}.gif`
: `/swf/c_images/album1584/${badge.badgeCode}.gif`
}
src={`/api/imaging/badge?source=local&code=${encodeURIComponent(badge.badgeCode)}`}
alt={badge.badgeCode}
className="h-5 w-5"
onError={(e) => {
+1 -3
View File
@@ -19,9 +19,7 @@ const schema = z.object({
export async function POST(request: Request) {
if (
request.headers.get("origin") !==
new URL(
process.env.APP_URL || process.env.NEXT_PUBLIC_APP_URL || request.url,
).origin ||
new URL(process.env.APP_URL || request.url).origin ||
!request.headers.get("content-type")?.startsWith("application/json")
)
return new Response(null, { status: 403 });
+5 -4
View File
@@ -1,7 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { resolveImagerBase } from "@/lib/imager";
import { resolveImagerBase } from "@/lib/runtime-asset-config";
const FIGURE_RE = /^[a-z]{2}-\d+(-\d+)?(\.[a-z]{2}-\d+(-\d+)?)*$/i;
const FIGURE_RE = /^[a-z]{2}-\d+(?:-\d+)*(?:\.[a-z]{2}-\d+(?:-\d+)*)*$/i;
const FIGURE_MAX_LEN = 512;
const FIGURE_MAX_PARTS = 24;
const UPSTREAM_TIMEOUT_MS = 10_000;
@@ -66,8 +66,9 @@ export async function GET(request: NextRequest) {
const imgFormat = searchParams.get("img_format");
if (imgFormat) params.set("img_format", imgFormat);
const upstream = resolveImagerBase();
const upstreamUrl = `${upstream}?${params.toString()}`;
const upstream = resolveImagerBase(new URL(request.url).origin);
const upstreamUrl = new URL(upstream);
for (const [key, value] of params) upstreamUrl.searchParams.set(key, value);
try {
const res = await fetch(upstreamUrl, {
+8 -2
View File
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from "next/server";
import { clientIp, rateLimit } from "@/lib/rate-limit";
import { resolveBadgeBase } from "@/lib/runtime-asset-config";
import { siteSettings } from "@/lib/services/site-settings";
/**
@@ -28,9 +29,14 @@ export async function GET(request: NextRequest) {
}
const configured = (
(await siteSettings.get("badge_base_url", "")) ?? ""
(resolveBadgeBase() || (await siteSettings.get("badge_base_url", ""))) ??
""
).trim();
const base = (configured || DEFAULT_BASE).replace(/\/+$/, "");
const fallback =
request.nextUrl.searchParams.get("source") === "local"
? "/swf/c_images/album1584"
: DEFAULT_BASE;
const base = (configured || fallback).replace(/\/+$/, "");
const isAbsolute = /^https?:\/\//i.test(base);
const target = `${base}/${code}.gif`;
const absoluteTarget = isAbsolute
+5 -4
View File
@@ -1,8 +1,8 @@
import { type NextRequest, NextResponse } from "next/server";
import sharp from "sharp";
import { resolveImagerBase } from "@/lib/imager";
import { resolveImagerBase } from "@/lib/runtime-asset-config";
const FIGURE_RE = /^([a-z]{2}-\d+)(\.[a-z]{2}-\d+)*$/i;
const FIGURE_RE = /^[a-z]{2}-\d+(?:-\d+)*(?:\.[a-z]{2}-\d+(?:-\d+)*)*$/i;
const FIGURE_MAX_LEN = 512;
const FIGURE_MAX_PARTS = 24;
const UPSTREAM_TIMEOUT_MS = 10_000;
@@ -82,8 +82,9 @@ export async function GET(request: NextRequest) {
const format = resolveFormat(searchParams.get("format"));
const upstream = resolveImagerBase();
const upstreamUrl = `${upstream}?${params.toString()}`;
const upstream = resolveImagerBase(new URL(request.url).origin);
const upstreamUrl = new URL(upstream);
for (const [key, value] of params) upstreamUrl.searchParams.set(key, value);
try {
const res = await fetch(upstreamUrl, {
+3
View File
@@ -2,6 +2,9 @@ import type { MetadataRoute } from "next";
import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
// Installation metadata must be resolved from the running container.
export const dynamic = "force-dynamic";
// Web app manifest — makes the hotel installable as a PWA (AtomCMS exposed PWA
// settings but the rewrite ships a real, themeable manifest). Name + theme
// colour follow the live website_settings; falls back to defaults with no DB.
+3
View File
@@ -1,5 +1,8 @@
import type { MetadataRoute } from "next";
// Installation metadata must be resolved from the running container.
export const dynamic = "force-dynamic";
export default function robots(): MetadataRoute.Robots {
const appUrl = process.env.APP_URL ?? "http://localhost:3000";
return {
+3
View File
@@ -2,6 +2,9 @@ import { desc } from "drizzle-orm";
import type { MetadataRoute } from "next";
import { db, Guilds, WebsiteArticles } from "@/lib/db";
// Installation metadata must be resolved from the running container.
export const dynamic = "force-dynamic";
// Built at request time — avoids competing with SSG workers for scarce DB
// connections during `next build` (pool timeouts killed deploy on sitemap).