fix: harden update pipeline and restore a clean production build
CI / check (push) Successful in 29s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m0s

- update-Nitrov3.sh: build CMS into .next-staging and swap atomically so a
  failed build never takes the live site down; auto-merge new variables
  from .env.example; validate env for duplicates/broken lines; restart the
  emulator/CMS only when rebuilt or unhealthy; fix step renumbering
- next.config.ts: support NEXT_DIST_DIR for staged production builds
- fix all TS errors (unused imports, missing tryDownloadCandidates helper)
  so tsc and the production build pass clean
- add Dockerfile/.dockerignore and switch docker-compose to a CMS container
- include prevailing UI/refactor changes (SurfaceCard, ticketing, tsconfig)
This commit is contained in:
openhands committed 2026-08-28 12:48:04 +02:00
1 parent 750fcb2e5c
commit 944e8ff1d8
26 files changed
+427 -876

No files matched your search

-191
View File
@@ -17,7 +17,6 @@ import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind";
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import { ensureDirectories } from "./furni-import";
import { convertSwfToNitro } from "./swf";
import { addSize32ToBundle } from "./swf/add-size32";
const REPAIR_CONCURRENCY = 6;
@@ -373,196 +372,6 @@ export async function repairMissingNitros(
* regenerated bundle is validated before it overwrites anything in the nitro
* directories.
*/
export async function reconvertAllNitros(
onEvent?: (evt: RepairNitroEvent) => void,
): Promise<{
succeeded: number;
failed: number;
skipped: number;
existing?: number;
}> {
let succeeded = 0;
let failed = 0;
let skipped = 0;
try {
await ensureDirectories();
} catch (err) {
onEvent?.({
type: "error",
message: `ensureDirectories failed: ${(err as Error).message}`,
});
return { succeeded, failed, skipped };
}
let targets: FurniAssetWriteTargets;
try {
targets = await getFurniAssetWriteTargets();
} catch (err) {
onEvent?.({
type: "error",
message: `getFurniAssetWriteTargets failed: ${(err as Error).message}`,
});
return { succeeded, failed, skipped };
}
const nitroDirs = uniqueDirs([
targets.nitroDir,
...targets.mirrorDirs.map((d) => d.nitroDir),
]);
// Build a kind lookup so we can skip furniture that has no asset (badges,
// song disks, etc).
const kindMap = new Map<
string,
{ item_name: string; type: string; interaction_type: string }
>();
try {
const [rows] = (await db.execute(sql`
SELECT item_name, type, interaction_type FROM items_base
`)) as unknown as [
Array<{ item_name: string; type: string; interaction_type: string }>,
unknown,
];
for (const r of rows) kindMap.set(r.item_name, r);
} catch (err) {
onEvent?.({
type: "error",
message: `Database query failed: ${(err as Error).message}`,
});
return { succeeded, failed, skipped };
}
const kindForBase = (base: string) => {
const exact = kindMap.get(base);
if (exact) return exact;
for (const [name, it] of kindMap) {
if (name.startsWith(`${base}*`)) return it;
}
return undefined;
};
// Visit every .nitro we already have on disk (primary dir + mirrored
// gamedata dirs) and re-convert the ones we have a local SWF for. Those are
// the bundles our own converter produced that may be missing size 32; we
// regenerate them and overwrite every nitro directory (including the live
// /var/www/Gamedata bundled/furniture). Bundles whose SWF is not available
// locally are left untouched — they are source-provided and already carry
// the sizes their assets define (a 32 frame can only exist if the furniture
// itself defines one in its assets).
const existingBases = new Set<string>();
for (const dir of nitroDirs) {
try {
const names = await fs.readdir(dir);
for (const n of names) {
if (!n.toLowerCase().endsWith(".nitro")) continue;
const base = n.replace(/\.nitro$/i, "");
existingBases.add(base);
// Classnames with dots are stored with underscores on disk.
existingBases.add(base.replace(/_/g, "."));
}
} catch {
/* ignore unreadable dir */
}
}
const existing = existingBases.size;
const basesToProcess: string[] = [];
for (const base of existingBases) {
if (existsSync(getRuntimePath(targets.swfDir, `${base}.swf`))) {
basesToProcess.push(base);
}
}
const total = basesToProcess.length;
onEvent?.({ type: "started", total });
await runPool(basesToProcess, REPAIR_CONCURRENCY, async (base, i) => {
const fileName = `${base}.nitro`;
const item = kindForBase(base);
if (item && NO_ASSET_KINDS.has(classifyCatalogItem(item))) {
skipped++;
onEvent?.({
type: "progress",
classname: base,
status: "skipped",
index: i,
total,
});
return;
}
const swfPath = getRuntimePath(targets.swfDir, `${base}.swf`);
let swf: Buffer;
try {
swf = await fs.readFile(swfPath);
} catch (err) {
skipped++;
onEvent?.({
type: "progress",
classname: base,
status: "no-swf",
index: i,
total,
message: (err as Error).message,
});
return;
}
try {
const result = convertSwfToNitro(swf, base);
if (!result.bundle) {
failed++;
onEvent?.({
type: "progress",
classname: base,
status: "convert-failed",
index: i,
total,
});
return;
}
// Validate the regenerated bundle before overwriting anything.
parseNitroBundle(result.bundle);
const errors = await writeNitroToDirs(result.bundle, nitroDirs, fileName);
if (errors.length > 0) {
failed++;
onEvent?.({
type: "progress",
classname: base,
status: "write-failed",
index: i,
total,
warnings: errors,
});
return;
}
succeeded++;
onEvent?.({
type: "progress",
classname: base,
status: "done",
index: i,
total,
warnings: result.warnings,
});
} catch (err) {
failed++;
onEvent?.({
type: "progress",
classname: base,
status: "convert-failed",
index: i,
total,
message: (err as Error).message,
});
}
});
onEvent?.({ type: "batch_complete", succeeded, failed, skipped, existing });
return { succeeded, failed, skipped, existing };
}
/**
* Synthesize a size-32 visualization on every existing furniture `.nitro` that