fix: harden update pipeline and restore a clean production build
- 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:
1 parent
750fcb2e5c
commit
944e8ff1d8
26 files changed
+427
-876
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user