@underpostnet/cyberia 3.2.80 → 3.2.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.env.example +34 -15
  2. package/.github/workflows/cyberia-client.cd.yml +13 -1
  3. package/.github/workflows/cyberia-server.cd.yml +13 -1
  4. package/.github/workflows/docker-image.cyberia-client.ci.yml +4 -4
  5. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +4 -4
  6. package/.github/workflows/docker-image.cyberia-server.ci.yml +4 -4
  7. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +4 -4
  8. package/.github/workflows/docker-image.engine-cyberia.ci.yml +3 -3
  9. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +3 -3
  10. package/.github/workflows/engine-cyberia.cd.yml +14 -3
  11. package/CHANGELOG.md +182 -1
  12. package/CLI-HELP.md +37 -16
  13. package/Dockerfile +1 -1
  14. package/Dockerfile.dev +1 -1
  15. package/Dockerfile.test +1 -1
  16. package/bin/cyberia.js +203 -49
  17. package/bin/deploy.js +18 -16
  18. package/bin/index.js +203 -49
  19. package/compose.env +34 -15
  20. package/deployment.yaml +1 -210
  21. package/docker-compose.yml +73 -62
  22. package/hardhat/package-lock.json +134 -126
  23. package/hardhat/package.json +2 -2
  24. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  25. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  26. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -210
  27. package/manifests/deployment/dd-cyberia-development/gateway.yaml +80 -0
  28. package/manifests/deployment/dd-cyberia-development/httproute.yaml +504 -0
  29. package/manifests/deployment/dd-cyberia-development/proxy.yaml +12 -12
  30. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +0 -82
  31. package/manifests/deployment/dd-cyberia-development/traffic-service.yaml +121 -0
  32. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  33. package/manifests/deployment/playwright/deployment.yaml +1 -1
  34. package/manifests/mongodb/kustomization.yaml +4 -1
  35. package/manifests/mongodb/statefulset.yaml +4 -0
  36. package/manifests/mongodb/storage-class.yaml +9 -2
  37. package/nginx.conf +86 -16
  38. package/package.json +2 -2
  39. package/proxy.yaml +12 -12
  40. package/pv-pvc.yaml +0 -82
  41. package/scripts/nat-iptables.sh +10 -4
  42. package/scripts/test-monitor.sh +4 -3
  43. package/src/api/cyberia-action/cyberia-action.model.js +1 -0
  44. package/src/api/cyberia-instance/cyberia-fallback-world.js +41 -14
  45. package/src/api/cyberia-instance/cyberia-instance-map.service.js +8 -12
  46. package/src/api/cyberia-instance/cyberia-portal-connector.js +7 -5
  47. package/src/api/cyberia-instance/cyberia-random-source.js +80 -0
  48. package/src/api/cyberia-instance/cyberia-world-generator.js +4 -3
  49. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +73 -0
  50. package/src/cli/cluster.js +740 -55
  51. package/src/cli/db.js +2 -2
  52. package/src/cli/deploy.js +1679 -174
  53. package/src/cli/docker-compose.js +19 -178
  54. package/src/cli/image.js +15 -6
  55. package/src/cli/index.js +124 -35
  56. package/src/cli/ipfs.js +82 -11
  57. package/src/cli/monitor.js +1 -1
  58. package/src/cli/repository.js +1 -1
  59. package/src/cli/run.js +2161 -420
  60. package/src/cli/secrets.js +969 -0
  61. package/src/cli/ssh.js +8 -28
  62. package/src/client/components/cyberia/InstanceSelectionView.js +11 -8
  63. package/src/client/components/cyberia/SharedDefaultsCyberia.js +5 -0
  64. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +106 -39
  65. package/src/client/public/cyberia-docs/ARCHITECTURE.md +18 -0
  66. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +44 -7
  67. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  68. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  69. package/src/client-builder/client-build.js +94 -11
  70. package/src/client-builder/ssr.js +27 -73
  71. package/src/db/mongo/MongoBootstrap.js +295 -54
  72. package/src/db/mongo/MongooseDB.js +47 -32
  73. package/src/index.js +1 -1
  74. package/src/projects/cyberia/besu-genesis-generator.js +3 -2
  75. package/src/projects/cyberia/hot-reload-trigger.js +3 -3
  76. package/src/projects/cyberia/instance-data.js +42 -1
  77. package/src/runtime/cyberia-client/Dockerfile +1 -1
  78. package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
  79. package/src/runtime/cyberia-server/Dockerfile +1 -1
  80. package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
  81. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  82. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  83. package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
  84. package/src/runtime/engine-cyberia/compose.env +34 -15
  85. package/src/runtime/engine-cyberia/docker-compose.yml +73 -62
  86. package/src/runtime/engine-cyberia/nginx.conf +86 -16
  87. package/src/server/conf.js +1208 -70
  88. package/src/server/cri.js +70 -0
  89. package/src/server/underpost-gateway.js +1073 -0
  90. package/src/server/underpost-ingress.js +364 -0
  91. package/test/cluster-instances.test.js +435 -0
  92. package/test/deploy-node-placement.test.js +45 -0
  93. package/test/instance-traffic-plan.test.js +710 -0
  94. package/test/sops-secret-store.test.js +612 -0
  95. package/test/underpost-gateway.test.js +469 -0
  96. package/test/underpost-ingress.test.js +253 -0
package/bin/deploy.js CHANGED
@@ -223,21 +223,23 @@ try {
223
223
  .readFileSync(`./engine-private/deploy/dd.router`, 'utf8')
224
224
  .split(`,`)
225
225
  .concat(['dd-cron'])) {
226
- if (fs.existsSync(`./engine-private/conf/${deployId}/build/development/deployment.yaml`))
227
- fs.copySync(
228
- `./engine-private/conf/${deployId}/build/development/deployment.yaml`,
229
- `./manifests/deployment/${deployId}-development/deployment.yaml`,
230
- );
231
- if (fs.existsSync(`./engine-private/conf/${deployId}/build/development/proxy.yaml`))
232
- fs.copySync(
233
- `./engine-private/conf/${deployId}/build/development/proxy.yaml`,
234
- `./manifests/deployment/${deployId}-development/proxy.yaml`,
235
- );
236
- if (fs.existsSync(`./engine-private/conf/${deployId}/build/development/pv-pvc.yaml`))
237
- fs.copySync(
238
- `./engine-private/conf/${deployId}/build/development/pv-pvc.yaml`,
239
- `./manifests/deployment/${deployId}-development/pv-pvc.yaml`,
240
- );
226
+ // The same set `deploy --build-manifest` mirrors. Anything missing here
227
+ // is a manifest the project repo silently stops carrying while the
228
+ // private build dir still has it.
229
+ for (const file of [
230
+ 'deployment.yaml',
231
+ 'proxy.yaml',
232
+ 'gateway.yaml',
233
+ 'httproute.yaml',
234
+ 'pv-pvc.yaml',
235
+ 'traffic-service.yaml',
236
+ 'grpc-service.yaml',
237
+ ]) {
238
+ const source = `./engine-private/conf/${deployId}/build/development/${file}`;
239
+ const target = `./manifests/deployment/${deployId}-development/${file}`;
240
+ if (fs.existsSync(source)) fs.copySync(source, target);
241
+ else fs.removeSync(target);
242
+ }
241
243
  shellExec(`node bin new --dev --default-conf --deploy-id ${deployId}`);
242
244
  }
243
245
  break;
@@ -763,7 +765,7 @@ nvidia/gpu-operator \
763
765
 
764
766
  case 'pw-conf': {
765
767
  const scriptPath = process.argv[3];
766
- shellExec(`kubectl delete configmap playwright-script`);
768
+ shellExec(`kubectl delete configmap playwright-script --ignore-not-found`);
767
769
  shellExec(`kubectl create configmap playwright-script \
768
770
  --from-file=script.js=${scriptPath} \
769
771
  --dry-run=client -o yaml | kubectl apply -f -
package/bin/index.js CHANGED
@@ -17,7 +17,7 @@ import { shellExec } from '../src/server/process.js';
17
17
  import { loggerFactory } from '../src/server/logger.js';
18
18
  import { generateBesuManifests, deployBesu, removeBesu } from '../src/projects/cyberia/besu-genesis-generator.js';
19
19
  import { DataBaseProviderService } from '../src/db/DataBaseProvider.js';
20
- import { loadConfServerJson } from '../src/server/conf.js';
20
+ import { etcHostFactory, loadConfServerJson, normalizeInstanceTopology } from '../src/server/conf.js';
21
21
  import {
22
22
  ObjectLayerEngine,
23
23
  resolveCanonicalCid,
@@ -51,6 +51,7 @@ import {
51
51
  } from '../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
52
52
 
53
53
  import {
54
+ DEFAULT_INSTANCE_CODE,
54
55
  ITEM_TYPES as itemTypes,
55
56
  DefaultCyberiaItems,
56
57
  } from '../src/client/components/cyberia/SharedDefaultsCyberia.js';
@@ -95,6 +96,22 @@ async function connectDbForChain({ envPath, mongoHost }) {
95
96
  /** @type {Function} */
96
97
  const logger = loggerFactory(import.meta);
97
98
 
99
+ const CYBERIA_DOCKER_HOST_ALIASES = ['cyberia-client', 'cyberia-server', 'engine-cyberia'];
100
+
101
+ const installCyberiaDockerHostAliases = () => {
102
+ try {
103
+ const { changed } = etcHostFactory(CYBERIA_DOCKER_HOST_ALIASES, {
104
+ append: true,
105
+ blockId: 'dd-cyberia-docker-compose',
106
+ });
107
+ return { aliases: CYBERIA_DOCKER_HOST_ALIASES, changed };
108
+ } catch (error) {
109
+ if (error?.code === 'EACCES' || error?.code === 'EPERM')
110
+ throw new Error('Cannot update /etc/hosts for Cyberia Docker aliases. Re-run the workflow as root.');
111
+ throw error;
112
+ }
113
+ };
114
+
98
115
  try {
99
116
  const program = new Command();
100
117
 
@@ -219,7 +236,6 @@ try {
219
236
  deployId,
220
237
  host,
221
238
  path,
222
- db,
223
239
  });
224
240
 
225
241
  await DataBaseProviderService.load({
@@ -1954,7 +1970,7 @@ try {
1954
1970
  ? db.host
1955
1971
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
1956
1972
 
1957
- logger.info('instance env', { env: options.envPath, deployId, host, path, db });
1973
+ logger.info('instance env', { env: options.envPath, deployId, host, path });
1958
1974
 
1959
1975
  await DataBaseProviderService.load({
1960
1976
  apis: [
@@ -3892,7 +3908,7 @@ try {
3892
3908
  ? db.host
3893
3909
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
3894
3910
 
3895
- logger.info('generate-saga', { deployId, host, path, db });
3911
+ logger.info('generate-saga', { deployId, host, path });
3896
3912
 
3897
3913
  await DataBaseProviderService.load({
3898
3914
  apis: [
@@ -4835,6 +4851,7 @@ try {
4835
4851
  '--clean',
4836
4852
  'Restore repositories to canonical state (git checkout Dockerfile, etc.) before updating compose.env',
4837
4853
  )
4854
+ .option('--test', 'Test DNS connectivity accross cyberia deployments')
4838
4855
  .option('--reset', 'Reset the development environment before updating compose.env')
4839
4856
  .action((options) => {
4840
4857
  if (options.reset) {
@@ -4848,6 +4865,25 @@ try {
4848
4865
  shellExec(`node bin run clean ./cyberia-client`);
4849
4866
  return;
4850
4867
  }
4868
+ if (options.test) {
4869
+ const testHosts = [
4870
+ 'localhost',
4871
+ 'localhost:4005',
4872
+ 'localhost:8081',
4873
+ 'localhost:8082',
4874
+ 'engine-cyberia',
4875
+ 'cyberia-server',
4876
+ 'cyberia-client',
4877
+ ];
4878
+ const testPaths = ['/', '/TEST', '/FOREST'];
4879
+ for (const host of testHosts) {
4880
+ for (const path of testPaths) {
4881
+ shellExec(`curl -L -v -i -s http://${host}${path} | head -n 10`, {
4882
+ silentOnError: true,
4883
+ });
4884
+ }
4885
+ }
4886
+ }
4851
4887
  const envPath = `./engine-private/conf/dd-cyberia/docker-compose/cyberia/compose.env`;
4852
4888
  const canonicalDevDockerfile = './src/runtime/engine-cyberia/Dockerfile.dev';
4853
4889
  fs.writeFileSync(
@@ -4930,7 +4966,7 @@ try {
4930
4966
  ? db.host
4931
4967
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
4932
4968
 
4933
- logger.info('drop-db', { deployId, host, path, db });
4969
+ logger.info('drop-db', { deployId, host, path });
4934
4970
 
4935
4971
  const cyberiaCollections = [
4936
4972
  'cyberia-entity',
@@ -4997,7 +5033,7 @@ try {
4997
5033
  // no funca
4998
5034
  if (options.loadTar) {
4999
5035
  for (const imageId of dockerImageIds)
5000
- if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.2.80.tar`);
5036
+ if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.2.90.tar`);
5001
5037
  return;
5002
5038
  }
5003
5039
  switch (id) {
@@ -5008,7 +5044,7 @@ node bin/build dd-cyberia --update-private
5008
5044
  node bin image --path src/runtime/engine-cyberia \
5009
5045
  --docker-compose --pull-base --build \
5010
5046
  --dockerfile-name Dockerfile.dev \
5011
- --image-name engine-cyberia-dev:v3.2.80 \
5047
+ --image-name engine-cyberia-dev:v3.2.90 \
5012
5048
  --image-out-path .
5013
5049
  `);
5014
5050
  break;
@@ -5019,7 +5055,7 @@ cp -f src/runtime/cyberia-server/Dockerfile.dev cyberia-server/Dockerfile.dev
5019
5055
  node bin image --path cyberia-server \
5020
5056
  --docker-compose --pull-base --build \
5021
5057
  --dockerfile-name Dockerfile.dev \
5022
- --image-name cyberia-server-dev:v3.2.80 \
5058
+ --image-name cyberia-server-dev:v3.2.90 \
5023
5059
  --image-out-path .
5024
5060
  `);
5025
5061
  break;
@@ -5029,7 +5065,7 @@ cp -f src/runtime/cyberia-client/Dockerfile.dev cyberia-client/Dockerfile.dev
5029
5065
  node bin image --path cyberia-client \
5030
5066
  --docker-compose --pull-base --build \
5031
5067
  --dockerfile-name Dockerfile.dev \
5032
- --image-name cyberia-client-dev:v3.2.80 \
5068
+ --image-name cyberia-client-dev:v3.2.90 \
5033
5069
  --image-out-path .
5034
5070
  `);
5035
5071
  break;
@@ -5038,6 +5074,10 @@ node bin image --path cyberia-client \
5038
5074
 
5039
5075
  for (const [cmd, action] of Object.entries(DOCKER_SCRIPTS))
5040
5076
  runner.command(cmd).action(() => {
5077
+ if (cmd === 'docker:up' || cmd === 'docker:up:build' || cmd === 'docker:restart') {
5078
+ const { aliases, changed } = installCyberiaDockerHostAliases();
5079
+ logger.info(`Docker host aliases ${changed ? 'installed' : 'already configured'}`, { aliases });
5080
+ }
5041
5081
  shellExec(action);
5042
5082
  });
5043
5083
 
@@ -5074,7 +5114,7 @@ node bin image --path cyberia-client \
5074
5114
  ? db.host
5075
5115
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
5076
5116
 
5077
- logger.info('seed-dialogues', { deployId, host, path, db });
5117
+ logger.info('seed-dialogues', { deployId, host, path });
5078
5118
 
5079
5119
  await DataBaseProviderService.load({ apis: ['cyberia-dialogue'], host, path, db });
5080
5120
 
@@ -5129,7 +5169,7 @@ node bin image --path cyberia-client \
5129
5169
  ? db.host
5130
5170
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
5131
5171
 
5132
- logger.info('seed-actions-quests', { deployId, host, path, db });
5172
+ logger.info('seed-actions-quests', { deployId, host, path });
5133
5173
 
5134
5174
  await DataBaseProviderService.load({ apis: ['cyberia-action', 'cyberia-quest'], host, path, db });
5135
5175
 
@@ -5185,7 +5225,7 @@ node bin image --path cyberia-client \
5185
5225
  ? db.host
5186
5226
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
5187
5227
 
5188
- logger.info('seed-skills', { deployId, host, path, db });
5228
+ logger.info('seed-skills', { deployId, host, path });
5189
5229
 
5190
5230
  await DataBaseProviderService.load({ apis: ['cyberia-skill'], host, path, db });
5191
5231
 
@@ -5245,7 +5285,7 @@ node bin image --path cyberia-client \
5245
5285
  ? db.host
5246
5286
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
5247
5287
 
5248
- logger.info('seed-entities', { deployId, host, path, db });
5288
+ logger.info('seed-entities', { deployId, host, path });
5249
5289
 
5250
5290
  await DataBaseProviderService.load({ apis: ['cyberia-entity-type-default'], host, path, db });
5251
5291
 
@@ -5353,6 +5393,84 @@ node bin image --path cyberia-client \
5353
5393
  logger.info('All semantic examples generated.');
5354
5394
  });
5355
5395
 
5396
+ // Instance id → project root. Single source of truth for the workloads this
5397
+ // workflow builds: both the k8s manifests and the status page artifacts each
5398
+ // project ships are resolved from this list plus conf.instances.json.
5399
+ const CYBERIA_INSTANCE_PROJECTS = [
5400
+ { id: 'mmo-client', rootPath: './cyberia-client' },
5401
+ { id: 'mmo-server', rootPath: './cyberia-server' },
5402
+ ];
5403
+ const CYBERIA_CONF_INSTANCES_PATH = './engine-private/conf/dd-cyberia/conf.instances.json';
5404
+ const CYBERIA_CONF_SSR_PATH = './engine-private/conf/dd-cyberia/conf.ssr.json';
5405
+ // Copy shared by the server and the client for a given status code. A status
5406
+ // without an entry falls back to its conf.ssr.json view title.
5407
+ const CYBERIA_STATUS_PAGE_META = {
5408
+ 404: {
5409
+ title: 'Cyberia — Sector Not Found',
5410
+ description: 'Cyberia Online 404 — the requested sector is not on the grid.',
5411
+ },
5412
+ };
5413
+
5414
+ /**
5415
+ * Reads the raw (unexpanded) conf.instances.json entries.
5416
+ * @returns {Array<object>} Instance entries, or an empty list when unavailable.
5417
+ */
5418
+ const readCyberiaConfInstances = () => {
5419
+ try {
5420
+ return JSON.parse(fs.readFileSync(CYBERIA_CONF_INSTANCES_PATH, 'utf8'));
5421
+ } catch (err) {
5422
+ logger.warn(`Could not read ${CYBERIA_CONF_INSTANCES_PATH}: ${err.message}`);
5423
+ return [];
5424
+ }
5425
+ };
5426
+
5427
+ /**
5428
+ * Resolves the SSR view that renders a status page. The view is declared in
5429
+ * conf.ssr.json as a route whose path is the bare status code (`/404`), the
5430
+ * same declaration the PWA build turns into `/404/index.html`.
5431
+ * @param {string} status - HTTP status code.
5432
+ * @returns {{ client: string, title: string }|null} View descriptor, or null when undeclared.
5433
+ */
5434
+ const resolveStatusPageView = (status) => {
5435
+ if (!fs.existsSync(CYBERIA_CONF_SSR_PATH)) return null;
5436
+ const confSSR = JSON.parse(fs.readFileSync(CYBERIA_CONF_SSR_PATH, 'utf8'));
5437
+ for (const clientConf of Object.values(confSSR))
5438
+ for (const view of clientConf?.views || []) if (view.path === `/${status}`) return view;
5439
+ return null;
5440
+ };
5441
+
5442
+ /**
5443
+ * Renders one custom status page to a static HTML artifact. The workloads no
5444
+ * longer serve error pages themselves — the document is carried into the
5445
+ * gateway config by `run instance-build-manifest`, so this only has to place
5446
+ * it at the `hostPath` the instance declares.
5447
+ * @param {string} status - HTTP status code.
5448
+ * @param {string} outputPath - Destination HTML path.
5449
+ * @param {boolean} [dev] - Render the development variant.
5450
+ * @returns {boolean} True when the artifact was rendered.
5451
+ */
5452
+ const buildCyberiaStatusPage = ({ status, outputPath, dev = false }) => {
5453
+ const view = resolveStatusPageView(status);
5454
+ const pagePath = `./src/client/ssr/views/${view?.client || `Cyberia${status}`}.js`;
5455
+ if (!fs.existsSync(pagePath)) {
5456
+ logger.warn(`[build-status-page] No SSR view for status ${status}; skipping`, { pagePath, outputPath });
5457
+ return false;
5458
+ }
5459
+ const meta = CYBERIA_STATUS_PAGE_META[status] || {};
5460
+ const title = meta.title || view?.title || `Cyberia — ${status}`;
5461
+ const description = meta.description || `Cyberia Online ${status}.`;
5462
+ shellExec(
5463
+ `node bin static --page ${pagePath}` +
5464
+ ` --output-path ${outputPath}` +
5465
+ ` --title '${title}'` +
5466
+ ` --favicon /favicon.ico` +
5467
+ ` --description '${description}'` +
5468
+ ` --lang en` +
5469
+ ` --env ${dev ? 'development' : 'production'}`,
5470
+ );
5471
+ return true;
5472
+ };
5473
+
5356
5474
  runner
5357
5475
  .command('build-manifest')
5358
5476
  .option(
@@ -5385,24 +5503,24 @@ node bin image --path cyberia-client \
5385
5503
  // A variant declared in conf.instances.json without the instance
5386
5504
  // directory is silently skipped so the Dockerfile never tries to
5387
5505
  // copy a non-existent directory and the container build does not fail.
5388
- const confInstancesPath = './engine-private/conf/dd-cyberia/conf.instances.json';
5389
5506
  const cyberiaInstancesDir = '/home/dd/cyberia-instances';
5390
5507
  let instanceCodes = 'amethyst-strata-expansion,FOREST'; // fallback
5508
+ const confInstancesEntries = readCyberiaConfInstances();
5391
5509
  try {
5392
- const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
5393
- const serverInstances = confInstances.filter((inst) => inst.runtime === 'cyberia-server');
5510
+ const serverInstances = confInstancesEntries.filter((inst) => inst.runtime === 'cyberia-server');
5394
5511
  const codes = new Set();
5395
5512
  for (const inst of serverInstances) {
5396
5513
  if (inst.multiInstance?.variants) {
5397
- for (const v of inst.multiInstance.variants) {
5398
- if (!v.code) continue;
5514
+ const topology = normalizeInstanceTopology(inst.multiInstance, `dd-cyberia/${inst.id}`);
5515
+ for (const v of topology.variants) {
5516
+ const code = v.path === '/' ? DEFAULT_INSTANCE_CODE : v.code;
5399
5517
  // Skip codes that have no on-disk instance backup dir
5400
- const instanceDir = `${cyberiaInstancesDir}/instances/${v.code}`;
5518
+ const instanceDir = `${cyberiaInstancesDir}/instances/${code}`;
5401
5519
  if (!fs.existsSync(instanceDir)) {
5402
- logger.info(`[build-manifest] Skipping code "${v.code}": no instance dir at ${instanceDir}`);
5520
+ logger.info(`[build-manifest] Skipping code "${code}": no instance dir at ${instanceDir}`);
5403
5521
  continue;
5404
5522
  }
5405
- codes.add(v.code);
5523
+ codes.add(code);
5406
5524
  }
5407
5525
  }
5408
5526
  }
@@ -5413,7 +5531,7 @@ node bin image --path cyberia-client \
5413
5531
  logger.warn(`[build-manifest] No valid instance codes found; keeping fallback: ${instanceCodes}`);
5414
5532
  }
5415
5533
  } catch (err) {
5416
- logger.warn(`[build-manifest] Could not read ${confInstancesPath}: ${err.message}; using fallback`);
5534
+ logger.warn(`[build-manifest] Could not read ${CYBERIA_CONF_INSTANCES_PATH}: ${err.message}; using fallback`);
5417
5535
  }
5418
5536
 
5419
5537
  // ── Update Dockerfile.dev + Dockerfile INSTANCE_CODES build arg ──────
@@ -5474,29 +5592,38 @@ node bin image --path cyberia-client \
5474
5592
  logger.warn(`[build-manifest] Could not update ${catalogPath}: ${err.message}`);
5475
5593
  }
5476
5594
 
5595
+ // ── Build SSR views ──────────────────────────────────────────────────
5596
+ // Status pages are rendered BEFORE the manifests: the gateway manifests
5597
+ // embed each document declared under an instance's `customStatusPages`,
5598
+ // so the artifact has to exist at its `hostPath` by manifest time.
5599
+ const statusPagesBuilt = [];
5600
+ for (const { id, rootPath } of CYBERIA_INSTANCE_PROJECTS) {
5601
+ const instance = confInstancesEntries.find((entry) => entry.id === id);
5602
+ for (const page of instance?.customStatusPages || []) {
5603
+ if (!page?.status || !page?.hostPath) continue;
5604
+ const outputPath = nodePath.normalize(`${rootPath}/${page.hostPath}`);
5605
+ if (buildCyberiaStatusPage({ status: page.status, outputPath, dev: isDev }))
5606
+ statusPagesBuilt.push({ instance: id, status: page.status, outputPath });
5607
+ }
5608
+ }
5609
+ logger.info('[build-manifest] Custom status pages built', statusPagesBuilt);
5610
+ shellExec(
5611
+ `node bin/cyberia run-workflow build-server-dashboard --output-path ./cyberia-server/public/index.html`,
5612
+ );
5613
+
5477
5614
  // ── Build dev manifests (always --kind --dev) ────────────────────────
5478
5615
  {
5479
5616
  const flags = `--kind --dev${nodeFlag}`;
5480
- shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-client,./cyberia-client' ${flags}`);
5481
- shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-server,./cyberia-server' ${flags}`);
5617
+ for (const { id, rootPath } of CYBERIA_INSTANCE_PROJECTS)
5618
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,${id},${rootPath}' ${flags}`);
5482
5619
  }
5483
5620
  // ── Build prod manifests (--kubeadm, no --dev) ───────────────────────
5484
5621
  if (!isDev) {
5485
5622
  const flags = `--kubeadm${nodeFlag}`;
5486
- shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-client,./cyberia-client' ${flags}`);
5487
- shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-server,./cyberia-server' ${flags}`);
5623
+ for (const { id, rootPath } of CYBERIA_INSTANCE_PROJECTS)
5624
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,${id},${rootPath}' ${flags}`);
5488
5625
  }
5489
5626
 
5490
- // ── Build SSR views ──────────────────────────────────────────────────
5491
- const env404 = isDev ? ' --dev' : '';
5492
- shellExec(
5493
- `node bin/cyberia run-workflow build-cyberia-404 --output-path ./cyberia-server/public/404.html${env404}`,
5494
- );
5495
- shellExec(`node bin/cyberia run-workflow build-cyberia-404 --output-path ./cyberia-client/bin/404.html${env404}`);
5496
- shellExec(
5497
- `node bin/cyberia run-workflow build-server-dashboard --output-path ./cyberia-server/public/index.html`,
5498
- );
5499
-
5500
5627
  // Copy canonical doc sources into the generated project READMEs.
5501
5628
  // Edit the canonical sources; never hand-edit these generated outputs.
5502
5629
  fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-CLIENT.md', './cyberia-client/README.md');
@@ -5565,27 +5692,54 @@ node bin image --path cyberia-client \
5565
5692
  );
5566
5693
  });
5567
5694
 
5695
+ runner
5696
+ .command('build-status-page')
5697
+ .requiredOption(
5698
+ '--status <status>',
5699
+ 'HTTP status code to render (must be declared as an SSR view in conf.ssr.json).',
5700
+ )
5701
+ .option('--dev', 'Build a development variant of the status page.')
5702
+ .option(
5703
+ '--output-path <path>',
5704
+ 'Output path for the rendered HTML. Defaults to the `hostPath` the mmo-server instance declares for the status.',
5705
+ )
5706
+ .description(
5707
+ 'Build one custom status page artifact. The SSR view is resolved from the conf.ssr.json route whose path is ' +
5708
+ 'the bare status code; the rendered document is served by the gateway, not by the workload.',
5709
+ )
5710
+ .action((options) => {
5711
+ const status = `${options.status}`;
5712
+ const statusPage = (
5713
+ readCyberiaConfInstances().find((entry) => entry.id === 'mmo-server')?.customStatusPages || []
5714
+ ).find((page) => `${page.status}` === status);
5715
+ const outputPath =
5716
+ options.outputPath || (statusPage ? nodePath.normalize(`./cyberia-server/${statusPage.hostPath}`) : null);
5717
+ if (!outputPath) {
5718
+ logger.error(`[build-status-page] No --output-path and no customStatusPages entry for status ${status}`);
5719
+ return;
5720
+ }
5721
+ buildCyberiaStatusPage({ status, outputPath, dev: !!options.dev });
5722
+ });
5723
+
5568
5724
  runner
5569
5725
  .command('build-cyberia-404')
5570
5726
  .option('--dev', 'Build a development variant of the 404 page.')
5571
5727
  .option(
5572
5728
  '--output-path <path>',
5573
5729
  'Output path for the rendered 404.html (default: ./cyberia-server/public/404.html). ' +
5574
- 'The same page is served, sub-path aware, by every instance variant of both the ' +
5575
- 'cyberia-server (Go static server) and cyberia-client (docker-driver).',
5730
+ 'The same page is served, sub-path aware, by the gateway for every instance variant of both the ' +
5731
+ 'cyberia-server and cyberia-client workloads.',
5732
+ )
5733
+ .description(
5734
+ 'Build the cyberpunk pixel-art "sector not found" (404) page shared by the Cyberia server + client. ' +
5735
+ 'Thin alias of `build-status-page --status 404`, kept as the entrypoint the instance CI workflows call.',
5576
5736
  )
5577
- .description('Build the cyberpunk pixel-art "sector not found" (404) page shared by the Cyberia server + client.')
5578
5737
  .action((options) => {
5579
- const outputPath = options.outputPath || './cyberia-server/public/404.html';
5580
- shellExec(
5581
- `node bin static --page ./src/client/ssr/views/Cyberia404.js` +
5582
- ` --output-path ${outputPath}` +
5583
- ` --title 'Cyberia — Sector Not Found'` +
5584
- ` --favicon /favicon.ico` +
5585
- ` --description 'Cyberia Online 404 — the requested sector is not on the grid.'` +
5586
- ` --lang en` +
5587
- ` --env ${options.dev ? 'development' : 'production'}`,
5588
- );
5738
+ buildCyberiaStatusPage({
5739
+ status: '404',
5740
+ outputPath: options.outputPath || './cyberia-server/public/404.html',
5741
+ dev: !!options.dev,
5742
+ });
5589
5743
  });
5590
5744
 
5591
5745
  // Passthrough check: if the user invoked a command that is OWNED by the
package/compose.env CHANGED
@@ -40,29 +40,27 @@ VALKEY_HOST=valkey-service
40
40
  VALKEY_PORT=6379
41
41
 
42
42
  # --- Cyberia instance wiring (compose service DNS) ---------------------------
43
- # cyberia-server-* -> engine-cyberia authority endpoints. INTERNAL only: these
44
- # use the docker service DNS (engine-cyberia) and must never leak to browser
45
- # clients. Per-instance INSTANCE_CODE is set on each generated server service
46
- # from conf.instances.json (multiInstance) — not here.
47
- ENGINE_API_BASE_URL=http://engine-cyberia:4005
48
- ENGINE_GRPC_ADDRESS=engine-cyberia:50051
43
+ # cyberia-server-* -> stable engine gateway for HTTP and the raw engine runtime
44
+ # for gRPC. The public engine-cyberia name belongs to nginx, avoiding ambiguous
45
+ # Docker DNS while keeping one stable HTTP endpoint for every module.
46
+ ENGINE_API_BASE_URL=http://engine-cyberia
47
+ ENGINE_GRPC_ADDRESS=engine-cyberia-runtime:50051
49
48
 
50
49
  # Client-visible Content Authority origin, forwarded to browser/Raylib clients
51
50
  # for every engine-cyberia content/asset/metadata request. Host-reachable
52
- # (docker publishes engine-cyberia :4005). Distinct from ENGINE_API_BASE_URL:
51
+ # (Docker publishes the raw engine runtime on host :4005). Unlike the internal
52
+ # ENGINE_API_BASE_URL, this value is consumed outside the Compose network:
53
53
  # the two responsibilities must never overwrite each other.
54
54
  ENGINE_PUBLIC_URL=http://localhost:4005
55
55
  ENGINE_GRPC_RELOAD_INTERVAL_SEC=300
56
56
 
57
57
  # --- Browser-facing client runtime config (injected by docker-driver.py) -----
58
58
  # The single WASM bundle resolves its instance from the URL sub-path and builds
59
- # its endpoints from these origins. They point at the nginx proxy on localhost:80
60
- # (the single gateway that path-routes WS -> server, /api + /assets -> engine),
61
- # NOT at the raw client/engine container ports — a WS to the client port (8082)
62
- # has no server behind it. So this must be the proxy origin, not :8082 / :4005.
63
- # http://localhost/ -> default instance, ws -> ws://localhost/ws
64
- # http://localhost/FOREST -> FOREST instance, ws -> ws://localhost/FOREST/ws
65
- CYBERIA_WS_ORIGIN=ws://localhost
59
+ # its endpoints from these origins. HTTP client/API traffic uses nginx on :80;
60
+ # WebSocket traffic uses the dedicated multi-path server gateway on :8081.
61
+ # http://localhost/ -> default, ws://localhost:8081/ws
62
+ # http://localhost/FOREST -> FOREST, ws://localhost:8081/FOREST/ws
63
+ CYBERIA_WS_ORIGIN=ws://localhost:8081
66
64
  CYBERIA_ENGINE_API_ORIGIN=http://localhost
67
65
  CYBERIA_DEFAULT_INSTANCE=amethyst-strata-expansion
68
66
 
@@ -82,6 +80,7 @@ NODE_OPTIONS=--max-old-space-size=8192
82
80
  BASE_API=api
83
81
  DEFAULT_DEPLOY_ID=dd-cyberia
84
82
  DEPLOY_ID=dd-cyberia
83
+ # Configuration/DB selector from conf.server.json; this is not a Docker DNS alias.
85
84
  DEFAULT_DEPLOY_HOST=www.cyberiaonline.com
86
85
  DEFAULT_DEPLOY_PATH=/
87
86
  DISABLE_API_RATE_LIMIT=true
@@ -131,7 +130,25 @@ IPFS_CLUSTER_API_URL=http://ipfs-cluster:9094
131
130
  # in docker-compose.yml). Override for a real/shared cluster.
132
131
  # CLUSTER_SECRET=
133
132
 
134
- # --- Host-published ports ----------------------------------------------------
133
+ # --- Local access ------------------------------------------------------------
134
+ # Client gateway: http://localhost/{,FOREST,TEST}
135
+ # Client port alias: http://localhost:8082/{,FOREST,TEST}
136
+ # Server gateway: http://localhost:8081/{,FOREST,TEST}
137
+ # Engine authority: http://localhost:4005
138
+ # Docker-network gateways:
139
+ # http://cyberia-client/{,FOREST,TEST}
140
+ # http://cyberia-server/{,FOREST,TEST}
141
+ # http://engine-cyberia/
142
+ #
143
+ # The *.localhost aliases normally resolve to loopback without configuration:
144
+ # http://client.cyberia.localhost/{,FOREST,TEST}
145
+ # http://server.cyberia.localhost/{,FOREST,TEST}
146
+ # http://engine.cyberia.localhost/
147
+ # `node bin/cyberia run-workflow docker:up` maintains this host entry (the
148
+ # aliases already work between containers):
149
+ # 127.0.0.1 cyberia-client cyberia-server engine-cyberia
150
+ #
151
+ # Host-published ports:
135
152
  PROXY_HTTP_PORT=80
136
153
  IPFS_API_PORT=5001
137
154
  IPFS_GATEWAY_PORT=8080
@@ -140,5 +157,7 @@ MONGO_HOST_PORT=27017
140
157
  VALKEY_NODEPORT=32079
141
158
  ENGINE_CYBERIA_REST_PORT=4005
142
159
  ENGINE_CYBERIA_GRPC_PORT=50051
160
+ # Multi-path nginx server gateway, not a single server container publication.
143
161
  CYBERIA_SERVER_PORT=8081
162
+ # Multi-path nginx client gateway; raw client containers remain internal.
144
163
  CYBERIA_CLIENT_PORT=8082