Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f66b954
Standardize postcss on 8.4.49 to address CVE in versions < 8.4.49 (#59)
Copilot Feb 18, 2026
102df74
Fix early-game UX issues: form placeholders, time visibility, complet…
Copilot Feb 18, 2026
e647265
feat: Add infrastructure-based production foundation (Phase 1-2) (#75)
Copilot Feb 19, 2026
878f01c
Fix OAuth redirect_uri mismatch between better-auth and provider conf…
Copilot Feb 19, 2026
360f8f3
Phase 3: Enforce storage capacity limits and production building requ…
Copilot Feb 19, 2026
7d00bce
Phase 4 & 5: Buildings Management UI + Preflight Validation + Determi…
Copilot Feb 19, 2026
37800c6
fix(auth): support single-origin sso routing
BENZOOgataga Feb 22, 2026
e3313ab
fix(web): preserve repeated proxy headers
BENZOOgataga Feb 22, 2026
aff1f22
fix(ci): apply migrations before APP_ROLE all startup
BENZOOgataga Feb 22, 2026
5cba719
fix(web): hide seeded example accounts in admin list
BENZOOgataga Feb 22, 2026
de18394
fix(api): allow admins on developer catalog read endpoints
BENZOOgataga Feb 22, 2026
1429d86
fix(web): accept redacted company cash in parsers
BENZOOgataga Feb 22, 2026
6850916
fix(api): support admin research catalog on developer page
BENZOOgataga Feb 22, 2026
0483391
fix(web): separate recipe input items across catalog views
BENZOOgataga Feb 22, 2026
bd41de8
fix(web): centralize item quantity labels for recipe outputs
BENZOOgataga Feb 22, 2026
0aba554
fix(web): resolve unknown item labels in market lists
BENZOOgataga Feb 22, 2026
26315cb
fix(web): scope market listings to company tradable items
BENZOOgataga Feb 22, 2026
9abebc7
fix(auth): support single-origin sso routing
BENZOOgataga Feb 22, 2026
e0f236b
fix(web): preserve repeated proxy headers
BENZOOgataga Feb 22, 2026
b9ac090
fix(ci): apply migrations before APP_ROLE all startup
BENZOOgataga Feb 22, 2026
7b63a1d
fix(web): hide seeded example accounts in admin list
BENZOOgataga Feb 22, 2026
f09d929
fix(api): allow admins on developer catalog read endpoints
BENZOOgataga Feb 22, 2026
b5f1e93
fix(web): accept redacted company cash in parsers
BENZOOgataga Feb 22, 2026
d462c17
fix(api): support admin research catalog on developer page
BENZOOgataga Feb 22, 2026
16d598b
fix(web): separate recipe input items across catalog views
BENZOOgataga Feb 22, 2026
2eb934d
fix(web): centralize item quantity labels for recipe outputs
BENZOOgataga Feb 22, 2026
f251dc0
fix(web): resolve unknown item labels in market lists
BENZOOgataga Feb 22, 2026
a2057be
fix(web): scope market listings to company tradable items
BENZOOgataga Feb 22, 2026
3ee45e0
docs(ops): use example domains and RFC5737 IPs in nginx docs
BENZOOgataga Feb 22, 2026
e547a83
docs(ops): drop API subdomain blocks from nginx sample
BENZOOgataga Feb 22, 2026
926b272
feat(web): add ALPHA preview disclaimer to footer version badge
BENZOOgataga Feb 22, 2026
e5197b7
feat(web): show alpha notice on version hover and overview
BENZOOgataga Feb 22, 2026
1dc209d
fix(web): remove hover helper text from version badge
BENZOOgataga Feb 22, 2026
cbb511f
fix(web): remove focus ring box from maintenance overlay
BENZOOgataga Feb 22, 2026
452b605
feat(web): link alpha version badge to Discord updates
BENZOOgataga Feb 22, 2026
e714406
fix(web): fetch Discord URL via runtime public-links endpoint
BENZOOgataga Feb 22, 2026
367c7a9
fix(docs): add guideline to avoid commits to main branch
BENZOOgataga Feb 22, 2026
bcced69
feat(web): replace static onboarding tutorial with guided walkthrough
BENZOOgataga Feb 22, 2026
459657a
fix(web): clarify overview metrics as world-level
BENZOOgataga Feb 22, 2026
e806a04
feat(web): begin guided tutorial with active company snapshot
BENZOOgataga Feb 22, 2026
6353f28
fix(sim): prevent zero-trade stalls from static bot books
BENZOOgataga Feb 22, 2026
5b4bcaa
fix(api): harden diagnostics missing-items service injection
BENZOOgataga Feb 22, 2026
0965f57
fix(ci): resolve root typecheck failures
BENZOOgataga Feb 22, 2026
4a66e9a
fix(web): wrap search params hooks in suspense
BENZOOgataga Feb 22, 2026
ef4321c
fix(ci): run release workflow only on main
BENZOOgataga Feb 22, 2026
ab88c3d
fix: stabilize prisma startup and restore phase 1-5 web UX
BENZOOgataga Feb 22, 2026
cf21d67
fix(db): run Prisma generate without relying on dotenv-cli shell binary
BENZOOgataga Feb 22, 2026
440c28d
fix(web): parse buildings definitions payload correctly
BENZOOgataga Feb 22, 2026
cf4b79f
fix(web): support legacy buildings definitions payload shape
BENZOOgataga Feb 22, 2026
fd42e37
Merge branch 'main' into canary
BENZOOgataga Feb 22, 2026
f4393bd
chore(release): cut v0.10.
BENZOOgataga Feb 22, 2026
4fc55d5
fix(db): sync static catalog for production deployments
BENZOOgataga Feb 22, 2026
ea9c76c
chore(release): cut v0.10.1
BENZOOgataga Feb 22, 2026
da88f8b
Merge remote-tracking branch 'origin/main' into canary
BENZOOgataga Feb 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
type: patch
area: db
summary: Add idempotent static catalog sync and run it on container startup
---

- Add `syncStaticCatalog` to upsert items, recipes, recipe inputs, research nodes, unlock links, and prerequisites without resetting world state.
- Ensure missing `CompanyRecipe` links are created for existing companies so newly added recipes become available.
- Add `pnpm sim:sync-static` and wire startup to run catalog sync automatically in `APP_ROLE=all` (or when `CORPSIM_SYNC_STATIC_DATA_ON_START=true`).
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,9 @@ All notable changes to CorpSim are documented in this file.
- [db] Run Prisma generate without relying on dotenv-cli shell binary
- [web] Fix buildings definitions API response parsing in web client
- [web] Accept legacy object-shaped buildings definitions payloads

## 0.10.1 - 2026-02-22

### What's Changed

- [db] Add idempotent static catalog sync and run it on container startup
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "corpsim",
"version": "0.10.0",
"version": "0.10.1",
"private": true,
"workspaces": [
"apps/*",
Expand Down Expand Up @@ -31,6 +31,7 @@
"sim:advance": "tsx scripts/sim-advance.ts",
"sim:reset": "tsx scripts/sim-reset.ts",
"sim:seed": "pnpm -C packages/db seed",
"sim:sync-static": "tsx scripts/sim-sync-static.ts",
"sim:stats": "tsx scripts/sim-stats.ts",
"release:entry": "node scripts/release-entry.mjs",
"release:check": "node scripts/check-release-entry.mjs",
Expand Down
239 changes: 239 additions & 0 deletions packages/db/src/seed-world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,245 @@ function isRecipeAutoUnlocked(recipeCode: string): boolean {
);
}

function chunkArray<T>(rows: T[], chunkSize: number): T[][] {
if (rows.length === 0) {
return [];
}

const chunks: T[][] = [];
for (let index = 0; index < rows.length; index += chunkSize) {
chunks.push(rows.slice(index, index + chunkSize));
}
return chunks;
}

export interface SyncStaticCatalogResult {
itemsSynced: number;
recipesSynced: number;
researchNodesSynced: number;
prerequisitesSynced: number;
companyRecipeLinksCreated: number;
}

export async function syncStaticCatalog(prisma: PrismaClient): Promise<SyncStaticCatalogResult> {
return prisma.$transaction(async (tx) => {
const itemsByKey: Record<string, { id: string; code: string; name: string }> = {};

for (const definition of ITEM_DEFINITIONS) {
const item = await tx.item.upsert({
where: { code: definition.code },
update: { name: definition.name },
create: {
code: definition.code,
name: definition.name
}
});
itemsByKey[definition.key] = item;
}

const recipesByKey: Record<string, { id: string; code: string }> = {};
for (const definition of RECIPE_DEFINITIONS) {
const outputItem = itemsByKey[definition.outputItemKey];
if (!outputItem) {
throw new Error(`seed recipe ${definition.code} references unknown output item key ${definition.outputItemKey}`);
}

const recipe = await tx.recipe.upsert({
where: { code: definition.code },
update: {
name: definition.name,
durationTicks: definition.durationTicks,
outputItemId: outputItem.id,
outputQuantity: definition.outputQuantity
},
create: {
code: definition.code,
name: definition.name,
durationTicks: definition.durationTicks,
outputItemId: outputItem.id,
outputQuantity: definition.outputQuantity
}
});

recipesByKey[definition.key] = recipe;

const expectedInputItemIds: string[] = [];
for (const input of definition.inputs) {
const inputItem = itemsByKey[input.itemKey];
if (!inputItem) {
throw new Error(`seed recipe ${definition.code} references unknown input item key ${input.itemKey}`);
}

expectedInputItemIds.push(inputItem.id);

await tx.recipeInput.upsert({
where: {
recipeId_itemId: {
recipeId: recipe.id,
itemId: inputItem.id
}
},
update: {
quantity: input.quantity
},
create: {
recipeId: recipe.id,
itemId: inputItem.id,
quantity: input.quantity
}
});
}

if (expectedInputItemIds.length === 0) {
await tx.recipeInput.deleteMany({
where: {
recipeId: recipe.id
}
});
} else {
await tx.recipeInput.deleteMany({
where: {
recipeId: recipe.id,
itemId: {
notIn: expectedInputItemIds
}
}
});
}
}

const researchNodesByKey: Record<string, { id: string; code: string }> = {};
for (const definition of RESEARCH_DEFINITIONS) {
const node = await tx.researchNode.upsert({
where: { code: definition.code },
update: {
name: definition.name,
description: definition.description,
costCashCents: definition.costCashCents,
durationTicks: definition.durationTicks
},
create: {
code: definition.code,
name: definition.name,
description: definition.description,
costCashCents: definition.costCashCents,
durationTicks: definition.durationTicks
}
});

researchNodesByKey[definition.key] = node;

const expectedUnlockRecipeIds: string[] = [];
for (const recipeKey of definition.unlockRecipeKeys) {
const recipe = recipesByKey[recipeKey];
if (!recipe) {
throw new Error(`research node ${definition.code} references unknown recipe key ${recipeKey}`);
}
expectedUnlockRecipeIds.push(recipe.id);
await tx.researchNodeUnlockRecipe.upsert({
where: {
nodeId_recipeId: {
nodeId: node.id,
recipeId: recipe.id
}
},
update: {},
create: {
nodeId: node.id,
recipeId: recipe.id
}
});
}

if (expectedUnlockRecipeIds.length === 0) {
await tx.researchNodeUnlockRecipe.deleteMany({
where: {
nodeId: node.id
}
});
} else {
await tx.researchNodeUnlockRecipe.deleteMany({
where: {
nodeId: node.id,
recipeId: {
notIn: expectedUnlockRecipeIds
}
}
});
}
}

await tx.researchPrerequisite.deleteMany();
await tx.researchPrerequisite.createMany({
data: RESEARCH_PREREQUISITES.map((entry) => {
const node = researchNodesByKey[entry.nodeKey];
const prerequisiteNode = researchNodesByKey[entry.prerequisiteKey];
if (!node) {
throw new Error(`research prerequisite references unknown node key ${entry.nodeKey}`);
}
if (!prerequisiteNode) {
throw new Error(`research prerequisite references unknown prerequisite key ${entry.prerequisiteKey}`);
}
return {
nodeId: node.id,
prerequisiteNodeId: prerequisiteNode.id
};
})
});

const allRecipes = Object.values(recipesByKey);
const autoUnlockedRecipeIdSet = new Set<string>(
allRecipes.filter((recipe) => isRecipeAutoUnlocked(recipe.code)).map((recipe) => recipe.id)
);

let companyRecipeLinksCreated = 0;
if (allRecipes.length > 0) {
const companies = await tx.company.findMany({
select: { id: true }
});

for (const company of companies) {
const rows = allRecipes.map((recipe) => ({
companyId: company.id,
recipeId: recipe.id,
isUnlocked: autoUnlockedRecipeIdSet.has(recipe.id)
}));

for (const batch of chunkArray(rows, 1000)) {
const created = await tx.companyRecipe.createMany({
data: batch,
skipDuplicates: true
});
companyRecipeLinksCreated += created.count;
}
}

const autoUnlockedRecipeIds = Array.from(autoUnlockedRecipeIdSet);
if (autoUnlockedRecipeIds.length > 0) {
await tx.companyRecipe.updateMany({
where: {
recipeId: {
in: autoUnlockedRecipeIds
},
isUnlocked: false
},
data: {
isUnlocked: true
}
});
}
}

return {
itemsSynced: ITEM_DEFINITIONS.length,
recipesSynced: RECIPE_DEFINITIONS.length,
researchNodesSynced: RESEARCH_DEFINITIONS.length,
prerequisitesSynced: RESEARCH_PREREQUISITES.length,
companyRecipeLinksCreated
};
});
}

export async function seedWorld(
prisma: PrismaClient,
options: SeedWorldOptions = {}
Expand Down
26 changes: 26 additions & 0 deletions scripts/sim-sync-static.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createPrismaClient, syncStaticCatalog } from "@corpsim/db";

async function main(): Promise<void> {
const prisma = createPrismaClient();

try {
const result = await syncStaticCatalog(prisma);
console.log(
[
"Static catalog sync complete.",
`Items: ${result.itemsSynced}`,
`Recipes: ${result.recipesSynced}`,
`Research nodes: ${result.researchNodesSynced}`,
`Prerequisites: ${result.prerequisitesSynced}`,
`Company recipe links created: ${result.companyRecipeLinksCreated}`
].join(" ")
);
} finally {
await prisma.$disconnect();
}
}

main().catch((error: unknown) => {
console.error("Static catalog sync failed", error);
process.exitCode = 1;
});
29 changes: 29 additions & 0 deletions scripts/start-container.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,37 @@ apply_migrations() {
pnpm exec prisma migrate deploy --schema packages/db/prisma/schema.prisma
}

is_truthy() {
local value="${1:-}"
local normalized
normalized="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
[[ "$normalized" == "1" || "$normalized" == "true" || "$normalized" == "yes" || "$normalized" == "on" ]]
}

should_sync_static_catalog() {
local configured="${CORPSIM_SYNC_STATIC_DATA_ON_START:-}"
if [[ -n "$configured" ]]; then
is_truthy "$configured"
return $?
fi

if [[ "$role" == "all" ]]; then
return 0
fi

return 1
}

sync_static_catalog() {
if should_sync_static_catalog; then
pnpm sim:sync-static
fi
}

run_all() {
# Ensure schema is current before starting long-running processes in single-container mode.
apply_migrations
sync_static_catalog

pnpm --filter @corpsim/api start &
api_pid=$!
Expand All @@ -59,6 +87,7 @@ run_all() {

case "$role" in
api)
sync_static_catalog
run_api
;;
web)
Expand Down
Loading