@underpostnet/cyberia 3.3.73 → 3.3.77

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 (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -15,13 +15,18 @@ import { loggerFactory } from '../../server/ops/logger.js';
15
15
  import { ObjectLayerRenderFramesDto } from '../object-layer-render-frames/object-layer-render-frames.model.js';
16
16
  import { FileFactory } from '../file/file.service.js';
17
17
  import fs from 'fs-extra';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import sharp from 'sharp';
21
+ import { validateStats } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
18
22
  import { ObjectLayerDto } from './object-layer.model.js';
19
23
  import { ObjectLayerEngine } from '../../projects/cyberia/object-layer.js';
20
24
  import { shellExec } from '../../server/runtime/process.js';
21
25
  import { DataQuery } from '../../server/storage/data-query.js';
22
26
  import { AtlasSpriteSheetService } from '../atlas-sprite-sheet/atlas-sprite-sheet.service.js';
27
+ import { AtlasSpriteSheetStore } from '../../projects/cyberia/atlas-sprite-sheet-store.js';
23
28
  import { IpfsClient } from '../../projects/cyberia/ipfs-client.js';
24
- import { createPinRecord, removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
29
+ import { removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
25
30
 
26
31
  /**
27
32
  * Logger instance for this module.
@@ -45,8 +50,8 @@ class ObjectLayerService {
45
50
  * - `/metadata/:itemType/:itemId` — Create an object layer from uploaded frames and metadata.
46
51
  * - Default — Create an object layer directly from the request body.
47
52
  *
48
- * The `/metadata` and default routes delegate to {@link ObjectLayerEngine.createObjectLayerDocuments}
49
- * for document creation, atlas generation, SHA-256 computation, and IPFS pinning.
53
+ * The `/metadata` and default routes delegate to {@link ObjectLayerEngine.persistObjectLayerDocuments}
54
+ * for the document write, atlas generation, SHA-256 computation, and IPFS pinning.
50
55
  *
51
56
  * @async
52
57
  * @function post
@@ -160,22 +165,12 @@ class ObjectLayerService {
160
165
  metadataOverride: req.body,
161
166
  });
162
167
 
163
- const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
168
+ const { objectLayer } = await ObjectLayerEngine.persistObjectLayerDocuments({
164
169
  ObjectLayer,
165
170
  ObjectLayerRenderFrames,
166
171
  objectLayerRenderFramesData,
167
172
  objectLayerData,
168
- createOptions: {
169
- generateAtlas: true,
170
- atlasServiceContext: {
171
- req,
172
- res,
173
- options,
174
- AtlasSpriteSheetService,
175
- IpfsClient,
176
- createPinRecord,
177
- },
178
- },
173
+ persistOptions: { options },
179
174
  });
180
175
 
181
176
  return objectLayer;
@@ -191,44 +186,32 @@ class ObjectLayerService {
191
186
  bodyData.data.render.cid = '';
192
187
  bodyData.data.render.metadataCid = '';
193
188
 
194
- // If has render frames, generate atlas + CIDs BEFORE creating the ObjectLayer
189
+ // Stage the atlas CIDs before the write, so the document is never stored without them.
195
190
  if (bodyData.objectLayerRenderFramesId) {
196
191
  const renderFramesDoc = await ObjectLayerRenderFrames.findById(bodyData.objectLayerRenderFramesId);
197
192
  if (renderFramesDoc) {
198
193
  try {
199
- const stagingOL = {
200
- data: bodyData.data,
201
- objectLayerRenderFramesId: renderFramesDoc,
202
- };
203
- const result = await AtlasSpriteSheetService.generate(
204
- { objectLayer: stagingOL, auth: req.auth },
205
- res,
194
+ const { atlasDoc, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
195
+ itemKey: bodyData.data.item.id,
196
+ objectLayerRenderFrames: renderFramesDoc,
206
197
  options,
207
- { skipObjectLayerSave: true },
208
- );
209
- bodyData.data.render.cid = result.atlasCid;
210
- bodyData.data.render.metadataCid = result.atlasMetadataCid;
211
- bodyData.atlasSpriteSheetId = result.atlasDoc._id;
198
+ });
199
+ bodyData.data.render.cid = atlasCid;
200
+ bodyData.data.render.metadataCid = atlasMetadataCid;
201
+ bodyData.atlasSpriteSheetId = atlasDoc._id;
212
202
  } catch (atlasError) {
213
203
  logger.error('Failed to auto-generate atlas for new ObjectLayer:', atlasError);
214
204
  }
215
205
  }
216
206
  }
217
207
 
218
- // Pin data JSON to IPFS
219
- try {
220
- const itemId = bodyData.data.item.id;
221
- const mfsPath = `/object-layer/${itemId}/${itemId}_data.json`;
222
- const ipfsResult = await IpfsClient.addJsonToIpfs(bodyData.data, `${itemId}_data.json`, mfsPath);
223
- if (ipfsResult) {
224
- bodyData.cid = ipfsResult.cid;
225
- await createPinRecord({ cid: ipfsResult.cid, resourceType: 'object-layer-data', mfsPath, options });
226
- }
227
- } catch (ipfsError) {
228
- logger.warn('Failed to pin data JSON to IPFS:', ipfsError.message);
229
- }
208
+ const dataCid = await ObjectLayerEngine.pinObjectLayerData({
209
+ data: bodyData.data,
210
+ itemId: bodyData.data.item.id,
211
+ options,
212
+ });
213
+ if (dataCid) bodyData.cid = dataCid;
230
214
 
231
- // Atomic create/replace – ObjectLayer is fully populated with all CIDs
232
215
  return await (await ObjectLayer.upsertByItemId(bodyData)).populate('objectLayerRenderFramesId');
233
216
  };
234
217
 
@@ -257,25 +240,33 @@ class ObjectLayerService {
257
240
  /** @type {import('./object-layer.model.js').ObjectLayerModel} */
258
241
  const ObjectLayer = DataBaseProviderService.getModel('ObjectLayer', options);
259
242
 
260
- // GET /search-item-ids?q=<partial> - Fast partial match search on data.item.id
243
+ // Object layers are addressed by item id, the key the editor navigates by.
244
+ const keyFilter = (key) => DataQuery.naturalKeyFilter('data.item.id', key);
245
+
246
+ // GET /search-item-ids - item identity, by prefix (`q`, for type-ahead) or by exact ids
247
+ // (`ids`, comma separated). Carries the item's type because that is what addresses its
248
+ // sprite directory; an id alone cannot be previewed.
261
249
  if (req.path.startsWith('/search-item-ids')) {
262
250
  const q = (req.query.q || '').trim();
263
- if (!q) return { itemIds: [] };
264
- // Escape regex special characters for safe partial matching
265
- const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
266
- const results = await ObjectLayer.find(
267
- { 'data.item.id': { $regex: escaped, $options: 'i' } },
268
- { 'data.item.id': 1, _id: 0 },
269
- )
270
- .limit(20)
251
+ const ids = String(req.query.ids || '')
252
+ .split(',')
253
+ .map((id) => id.trim())
254
+ .filter(Boolean);
255
+ if (!q && ids.length === 0) return { items: [] };
256
+ const query = ids.length
257
+ ? { 'data.item.id': { $in: ids } }
258
+ : // Escape regex special characters for safe partial matching
259
+ { 'data.item.id': { $regex: q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } };
260
+ const results = await ObjectLayer.find(query, { 'data.item.id': 1, 'data.item.type': 1, _id: 0 })
261
+ .limit(ids.length ? ids.length : 20)
271
262
  .lean();
272
- const itemIds = [...new Set(results.map((r) => r.data.item.id))];
273
- return { itemIds };
263
+ const byId = new Map(results.map((r) => [r.data.item.id, r.data.item.type || '']));
264
+ return { items: [...byId].map(([id, type]) => ({ id, type })) };
274
265
  }
275
266
 
276
267
  // GET /frame-counts/:id - Get frame counts for each direction using numeric codes
277
268
  if (req.path.startsWith('/frame-counts/')) {
278
- const objectLayer = await ObjectLayer.findById(req.params.id)
269
+ const objectLayer = await ObjectLayer.findOne(keyFilter(req.params.id))
279
270
  .select(ObjectLayerDto.select.getMetadata())
280
271
  .populate('objectLayerRenderFramesId');
281
272
  if (!objectLayer) {
@@ -318,7 +309,7 @@ class ObjectLayerService {
318
309
 
319
310
  // GET /render/:id - Get only render data for specific object layer
320
311
  if (req.path.startsWith('/render/')) {
321
- const objectLayer = await ObjectLayer.findById(req.params.id)
312
+ const objectLayer = await ObjectLayer.findOne(keyFilter(req.params.id))
322
313
  .select(ObjectLayerDto.select.getRender())
323
314
  .populate('objectLayerRenderFramesId');
324
315
  if (!objectLayer) {
@@ -333,7 +324,7 @@ class ObjectLayerService {
333
324
 
334
325
  // GET /metadata/:id - Get only metadata (no render frames/colors) for specific object layer
335
326
  if (req.path.startsWith('/metadata/')) {
336
- const objectLayer = await ObjectLayer.findById(req.params.id)
327
+ const objectLayer = await ObjectLayer.findOne(keyFilter(req.params.id))
337
328
  .select(ObjectLayerDto.select.getMetadata())
338
329
  .populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
339
330
  if (!objectLayer) {
@@ -385,22 +376,14 @@ class ObjectLayerService {
385
376
  }
386
377
 
387
378
  // GET / - Get paginated list of object layers
388
- const id = req.params.id || req.query.id;
389
- logger.info(`ObjectLayerService.get - filtering check - id: ${id}`);
390
- if (id && id !== 'undefined' && !['render', 'metadata', 'frame-counts'].includes(id)) {
391
- try {
392
- const objectLayer = await ObjectLayer.findById(id)
393
- .select(ObjectLayerDto.select.get())
394
- .populate('atlasSpriteSheetId', 'cid')
395
- .populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
396
- if (objectLayer) {
397
- logger.info(`ObjectLayerService.get - found record by id: ${id}`);
398
- return { data: [objectLayer], total: 1, page: 1, totalPages: 1 };
399
- }
400
- logger.warn(`ObjectLayerService.get - record NOT found for id: ${id}`);
401
- } catch (e) {
402
- logger.warn(`Invalid ID format or not found: ${id}`);
403
- }
379
+ const key = req.params.id || req.query.id;
380
+ if (key && key !== 'undefined' && !['render', 'metadata', 'frame-counts'].includes(key)) {
381
+ const objectLayer = await ObjectLayer.findOne(keyFilter(key))
382
+ .select(ObjectLayerDto.select.get())
383
+ .populate('atlasSpriteSheetId', 'cid')
384
+ .populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
385
+ if (objectLayer) return { data: [objectLayer], total: 1, page: 1, totalPages: 1 };
386
+ logger.warn(`ObjectLayerService.get - no object layer for key: ${key}`);
404
387
  }
405
388
 
406
389
  const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
@@ -493,9 +476,14 @@ class ObjectLayerService {
493
476
  // Create temporary output file path
494
477
  const tempOutputPath = `${framesFolder}/${tmpOutputFileName}`;
495
478
 
479
+ // img2webp takes one frame size, and frames of one direction may differ: the game
480
+ // stretches each to the entity's cell box, so the preview stretches them to the
481
+ // largest one — nearest-neighbour, since a scaled frame is still pixel art.
482
+ const stagedFolder = await ObjectLayerService.stageUniformFrames(framesFolder, pngFiles);
483
+
496
484
  try {
497
485
  // Change to the frames directory and execute img2webp command
498
- const cmd = `cd "${framesFolder}" && img2webp -d ${frameDuration} -loop 0 *.png -o ${tmpOutputFileName}`;
486
+ const cmd = `cd "${stagedFolder ?? framesFolder}" && img2webp -d ${frameDuration} -loop 0 *.png -o "${path.resolve(tempOutputPath)}"`;
499
487
 
500
488
  logger.info(`Executing command: ${cmd}`);
501
489
 
@@ -546,9 +534,41 @@ class ObjectLayerService {
546
534
  }
547
535
 
548
536
  throw error;
537
+ } finally {
538
+ if (stagedFolder) await fs.remove(stagedFolder);
549
539
  }
550
540
  };
551
541
 
542
+ /**
543
+ * Stages copies of the frames resized to one common size when they differ, or returns null
544
+ * when they already share one and can be encoded in place.
545
+ * @function stageUniformFrames
546
+ * @memberof ObjectLayerService
547
+ * @param {string} framesFolder - Directory holding the direction's PNG frames.
548
+ * @param {string[]} pngFiles - Frame file names, in animation order.
549
+ * @returns {Promise<string|null>} Temporary directory with the resized frames, or null.
550
+ */
551
+ static stageUniformFrames = async (framesFolder, pngFiles) => {
552
+ const sizes = await Promise.all(
553
+ pngFiles.map(async (file) => {
554
+ const { width, height } = await sharp(`${framesFolder}/${file}`).metadata();
555
+ return { file, width, height };
556
+ }),
557
+ );
558
+ const width = Math.max(...sizes.map((size) => size.width));
559
+ const height = Math.max(...sizes.map((size) => size.height));
560
+ if (sizes.every((size) => size.width === width && size.height === height)) return null;
561
+
562
+ const stagedFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'ol-webp-'));
563
+ for (const { file } of sizes) {
564
+ await sharp(`${framesFolder}/${file}`)
565
+ .resize(width, height, { fit: 'fill', kernel: 'nearest' })
566
+ .toFile(`${stagedFolder}/${file}`);
567
+ }
568
+ logger.info(`Staged ${pngFiles.length} frame(s) at ${width}x${height} for webp encoding`);
569
+ return stagedFolder;
570
+ };
571
+
552
572
  /**
553
573
  * PUT handler for updating object layers, their frame images, and metadata.
554
574
  *
@@ -557,8 +577,8 @@ class ObjectLayerService {
557
577
  * - `/:id/metadata/:itemType/:itemId` — Update metadata and reprocess all frames.
558
578
  * - `/:id` — Standard update from request body.
559
579
  *
560
- * The `/metadata` route delegates to {@link ObjectLayerEngine.updateObjectLayerDocuments}
561
- * for document update, atlas regeneration, SHA-256 computation, and IPFS pinning.
580
+ * The `/metadata` route delegates to {@link ObjectLayerEngine.persistObjectLayerDocuments}
581
+ * for the document write, atlas regeneration, SHA-256 computation, and IPFS pinning.
562
582
  *
563
583
  * @async
564
584
  * @function put
@@ -652,7 +672,6 @@ class ObjectLayerService {
652
672
 
653
673
  // PUT /:id/metadata/:itemType/:itemId - Update object layer metadata and reprocess all frames
654
674
  if (req.path.includes('/metadata/')) {
655
- const objectLayerId = req.params.id;
656
675
  const itemType = req.params.itemType;
657
676
  const itemId = req.params.itemId;
658
677
 
@@ -683,24 +702,12 @@ class ObjectLayerService {
683
702
  metadataOverride: req.body,
684
703
  });
685
704
 
686
- // Update documents using engine method (with atlas generation)
687
- const { objectLayer } = await ObjectLayerEngine.updateObjectLayerDocuments({
688
- objectLayerId,
705
+ const { objectLayer } = await ObjectLayerEngine.persistObjectLayerDocuments({
689
706
  ObjectLayer,
690
707
  ObjectLayerRenderFrames,
691
708
  objectLayerRenderFramesData,
692
709
  objectLayerData,
693
- updateOptions: {
694
- generateAtlas: true,
695
- atlasServiceContext: {
696
- req,
697
- res,
698
- options,
699
- AtlasSpriteSheetService,
700
- IpfsClient,
701
- createPinRecord,
702
- },
703
- },
710
+ persistOptions: { options },
704
711
  });
705
712
 
706
713
  return objectLayer;
@@ -715,25 +722,19 @@ class ObjectLayerService {
715
722
  // Apply body updates to staging data in memory
716
723
  const updateData = { ...req.body };
717
724
  const stagingData = updateData.data || existingOL.data.toObject();
725
+ stagingData.stats = validateStats(stagingData.stats);
718
726
  if (!stagingData.render) stagingData.render = {};
719
727
 
720
- // Use existing render frames if available
721
- const renderFramesData = existingOL.objectLayerRenderFramesId;
722
- if (renderFramesData) {
728
+ if (existingOL.objectLayerRenderFramesId) {
723
729
  try {
724
- const stagingOL = {
725
- data: stagingData,
726
- objectLayerRenderFramesId: renderFramesData,
727
- };
728
- const result = await AtlasSpriteSheetService.generate(
729
- { objectLayer: stagingOL, auth: req.auth },
730
- res,
730
+ const { atlasDoc, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
731
+ itemKey: stagingData.item.id,
732
+ objectLayerRenderFrames: existingOL.objectLayerRenderFramesId,
731
733
  options,
732
- { skipObjectLayerSave: true },
733
- );
734
- stagingData.render.cid = result.atlasCid;
735
- stagingData.render.metadataCid = result.atlasMetadataCid;
736
- updateData.atlasSpriteSheetId = result.atlasDoc._id;
734
+ });
735
+ stagingData.render.cid = atlasCid;
736
+ stagingData.render.metadataCid = atlasMetadataCid;
737
+ updateData.atlasSpriteSheetId = atlasDoc._id;
737
738
  } catch (atlasError) {
738
739
  logger.error('Failed to auto-update atlas for ObjectLayer:', atlasError);
739
740
  }
@@ -742,25 +743,17 @@ class ObjectLayerService {
742
743
  updateData.data = stagingData;
743
744
  updateData.sha256 = ObjectLayerEngine.computeSha256(stagingData);
744
745
 
745
- // Pin data JSON to IPFS
746
- try {
747
- const itemId = stagingData.item.id;
748
- const mfsPath = `/object-layer/${itemId}/${itemId}_data.json`;
749
- const ipfsResult = await IpfsClient.addJsonToIpfs(stagingData, `${itemId}_data.json`, mfsPath);
750
- if (ipfsResult) {
751
- updateData.cid = ipfsResult.cid;
752
- await createPinRecord({ cid: ipfsResult.cid, resourceType: 'object-layer-data', mfsPath, options });
753
- }
754
- } catch (ipfsError) {
755
- logger.warn('Failed to pin data JSON to IPFS:', ipfsError.message);
756
- }
746
+ const dataCid = await ObjectLayerEngine.pinObjectLayerData({
747
+ data: stagingData,
748
+ itemId: stagingData.item.id,
749
+ options,
750
+ });
751
+ if (dataCid) updateData.cid = dataCid;
757
752
 
758
- // Atomic update with all CIDs populated
759
- let updatedObjectLayer = await ObjectLayer.findByIdAndUpdate(req.params.id, updateData, {
753
+ return await ObjectLayer.findByIdAndUpdate(req.params.id, updateData, {
760
754
  returnDocument: 'after',
755
+ runValidators: true,
761
756
  }).populate('objectLayerRenderFramesId');
762
-
763
- return updatedObjectLayer;
764
757
  };
765
758
 
766
759
  /**
package/src/cli/app.js CHANGED
@@ -205,14 +205,25 @@ class UnderpostApp {
205
205
  // The resolution loadConf performed, not `ociEnv()`, which forces the overlay on: a
206
206
  // skipped overlay and an applied one were indistinguishable in the pod log.
207
207
  const resolved = deployEnvContentFactory(deployId, context.env, `${context.args?.['sub-conf'] ?? ''}`.trim());
208
+ const inContainer = Underpost.state.isInsideContainer();
208
209
  logger.info('Deployment environment loaded', {
209
210
  deployId,
210
211
  instanceId: instanceId || null,
211
212
  source: envPath,
212
- inContainer: Underpost.state.isInsideContainer(),
213
+ inContainer,
213
214
  ociOverlay: resolved.overlay,
214
215
  keys: Object.keys(values).length,
215
216
  });
217
+ // A container that resolves no overlay runs on the host-scoped endpoints of the base file —
218
+ // a loopback database, a local cache — and fails minutes later at the first connection,
219
+ // pointing at the endpoint rather than at the conf checkout that is missing the overlay.
220
+ // Named rather than thrown: a deployment is allowed to have no overlay for an environment.
221
+ if (inContainer && !instanceId && !resolved.overlay)
222
+ logger.warn('No OCI env overlay resolved; container is running on host-scoped values', {
223
+ deployId,
224
+ env: context.env,
225
+ expected: deployOciEnvFilePath(deployId, context.env),
226
+ });
216
227
  return { source: envPath, keys: Object.keys(values).length };
217
228
  },
218
229
 
package/src/cli/client.js CHANGED
@@ -83,7 +83,7 @@ class UnderpostClient {
83
83
  const env = deployEnvFactory(options, process.env.NODE_ENV || 'development');
84
84
  process.env.NODE_ENV = env;
85
85
  if (options.mergeZip) {
86
- mergeClientBuildZip({
86
+ await mergeClientBuildZip({
87
87
  buildPrefix: options.mergeZip,
88
88
  logger,
89
89
  });
@@ -91,7 +91,7 @@ class UnderpostClient {
91
91
  }
92
92
 
93
93
  if (options.unzip) {
94
- unzipClientBuild({
94
+ await unzipClientBuild({
95
95
  buildPrefix: options.unzip,
96
96
  logger,
97
97
  });
@@ -31,10 +31,18 @@ import {
31
31
  } from '../server/network/underpost-ingress.js';
32
32
  import { MONGODB_DEFAULT_REPLICA_COUNT } from '../db/mongo/MongooseDB.js';
33
33
  import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
34
+ import { MongoExpress } from '../db/mongo/MongoExpress.js';
34
35
  import os from 'os';
35
36
  import fs from 'fs-extra';
36
37
  import Underpost from '../index.js';
37
38
 
39
+ /**
40
+ * @constant CERT_MANAGER_VERSION
41
+ * @description Pinned cert-manager chart version.
42
+ * @memberof UnderpostCluster
43
+ */
44
+ const CERT_MANAGER_VERSION = 'v1.17.0';
45
+
38
46
  const logger = loggerFactory(import.meta);
39
47
 
40
48
  const GATEWAY_API_RELEASE = 'v1.5.1';
@@ -82,6 +90,7 @@ class UnderpostCluster {
82
90
  * @param {object} [options] - Configuration options for cluster initialization.
83
91
  * @param {boolean} [options.mongodb=false] - Deploy MongoDB.
84
92
  * @param {boolean} [options.mongodb4=false] - Deploy MongoDB 4.4.
93
+ * @param {boolean} [options.mongoExpress=false] - Deploy the mongo-express web client for inspecting the MongoDB StatefulSet collections.
85
94
  * @param {string} [options.serviceHost=''] - Set a custom host/IP for exposed MongoDB and Valkey clients.
86
95
  * @param {boolean} [options.mariadb=false] - Deploy MariaDB.
87
96
  * @param {boolean} [options.mysql=false] - Deploy MySQL.
@@ -129,6 +138,7 @@ class UnderpostCluster {
129
138
  options = {
130
139
  mongodb: false,
131
140
  mongodb4: false,
141
+ mongoExpress: false,
132
142
  serviceHost: '',
133
143
  mariadb: false,
134
144
  mysql: false,
@@ -509,6 +519,10 @@ class UnderpostCluster {
509
519
  });
510
520
  }
511
521
 
522
+ // After both StatefulSet branches, so one invocation can bring up MongoDB and its inspector
523
+ // and the client reads the auth mode from the set this run just applied.
524
+ if (options.mongoExpress) await MongoExpress.deploy({ namespace: options.namespace, underpostRoot, options });
525
+
512
526
  // Installing one stack must never break the other. Whichever is already
513
527
  // present decides whether this install takes the node's 80/443 for itself
514
528
  // or joins the underpost ingress, so it is read before anything is applied.
@@ -663,22 +677,12 @@ EOF
663
677
  }
664
678
  }
665
679
 
666
- if (options.certManager) {
667
- if (!Underpost.kubectl.get('cert-manager').find((p) => p.STATUS === 'Running')) {
668
- shellExec(`helm repo add jetstack https://charts.jetstack.io --force-update`);
669
- shellExec(
670
- `helm install cert-manager jetstack/cert-manager \
671
- --namespace cert-manager \
672
- --create-namespace \
673
- --version v1.17.0 \
674
- --set crds.enabled=true`,
675
- );
676
- }
677
-
678
- const letsEncName = 'letsencrypt-prod';
679
- shellExec(`sudo kubectl delete ClusterIssuer ${letsEncName} --ignore-not-found`);
680
- shellExec(`sudo kubectl apply -f ${underpostRoot}/manifests/${letsEncName}.yaml -n ${options.namespace}`);
681
- }
680
+ if (options.certManager)
681
+ Underpost.cluster.installCertManager({
682
+ namespace: options.namespace,
683
+ env: options.dev ? 'development' : 'production',
684
+ solver: Underpost.cluster.acmeSolverFactory({ options, presence: Underpost.cluster.ingressStackPresence() }),
685
+ });
682
686
  },
683
687
 
684
688
  /**
@@ -1093,6 +1097,57 @@ EOF
1093
1097
  return true;
1094
1098
  },
1095
1099
 
1100
+ /**
1101
+ * @method acmeSolverFactory
1102
+ * @description Which ingress stack answers the HTTP-01 challenge.
1103
+ *
1104
+ * Read from the cluster, like {@link UnderpostCluster.ingressStackPresence}: the
1105
+ * `cluster` runner installs the issuer in a separate invocation that carries no stack
1106
+ * flag, so the flags alone would always name the Contour solver. `--contour` is the
1107
+ * only explicit override; a cluster with the Gateway API data plane installed routes
1108
+ * every hostname through it, so an Ingress solver there is unreachable.
1109
+ * @param {object} params
1110
+ * @param {object} [params.options] - Cluster init options.
1111
+ * @param {{contour: boolean, gateway: boolean}} params.presence - Installed stacks.
1112
+ * @returns {'gateway'|'ingress'} Solver kind.
1113
+ * @memberof UnderpostCluster
1114
+ */
1115
+ acmeSolverFactory({ options = {}, presence }) {
1116
+ if (options.contour === true && options.gatewayApi !== true) return 'ingress';
1117
+ return options.gatewayApi === true || presence.gateway ? 'gateway' : 'ingress';
1118
+ },
1119
+
1120
+ /**
1121
+ * @method installCertManager
1122
+ * @description Installs or upgrades cert-manager and applies the Let's Encrypt
1123
+ * ClusterIssuer for the stack in use.
1124
+ *
1125
+ * `upgrade --install` rather than a one-time install: the Gateway API solver is
1126
+ * ignored by a controller that was not started with `enableGatewayAPI`, so a cluster
1127
+ * whose cert-manager predates the Gateway API migration must be re-configured, not
1128
+ * skipped because a pod is Running. The version is pinned, so on a cluster already at
1129
+ * it the upgrade is a no-op.
1130
+ * @param {object} params
1131
+ * @param {string} [params.namespace] - Namespace the deploys' Gateways live in.
1132
+ * @param {string} [params.env] - Environment the routed deploys publish under.
1133
+ * @param {'gateway'|'ingress'} params.solver - Which stack answers the challenge.
1134
+ * @returns {{solver: string, parentRefs: Array<{name: string, namespace: string}>}} What was applied.
1135
+ * @memberof UnderpostCluster
1136
+ */
1137
+ installCertManager({ namespace = 'default', env = 'production', solver }) {
1138
+ const gatewayValues =
1139
+ solver === 'gateway'
1140
+ ? ' --set config.apiVersion=controller.config.cert-manager.io/v1alpha1' +
1141
+ ' --set config.kind=ControllerConfiguration --set config.enableGatewayAPI=true'
1142
+ : '';
1143
+ shellExec(`helm repo add jetstack https://charts.jetstack.io --force-update`);
1144
+ shellExec(
1145
+ `helm upgrade --install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace` +
1146
+ ` --version ${CERT_MANAGER_VERSION} --set crds.enabled=true${gatewayValues} --wait --timeout 5m`,
1147
+ );
1148
+ return Underpost.deploy.applyLetsEncryptIssuer({ solver, namespace, env });
1149
+ },
1150
+
1096
1151
  /**
1097
1152
  * @method ingressStackPresence
1098
1153
  * @description Which ingress data planes are installed right now.
@@ -1351,14 +1406,21 @@ EOF
1351
1406
  }
1352
1407
 
1353
1408
  try {
1354
- shellExec(`kubectl apply -f - -n ${namespace} <<'EOF'
1409
+ // The manifest carries the whole nginx.conf: echoing it as the command line
1410
+ // buries the lines that follow it — the fallback probes above all — under
1411
+ // a page of routing tables on every sync.
1412
+ logger.info('Applying the underpost ingress manifests', { namespace, node: ingressNode });
1413
+ shellExec(
1414
+ `kubectl apply -f - -n ${namespace} <<'EOF'
1355
1415
  ${underpostIngressManifestsFactory({
1356
1416
  namespace,
1357
1417
  conf,
1358
1418
  nodeName: ingressNode,
1359
1419
  })}
1360
1420
  EOF
1361
- `);
1421
+ `,
1422
+ { disableLog: true },
1423
+ );
1362
1424
  } catch (error) {
1363
1425
  if (shouldHotReload)
1364
1426
  execIngress(`sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`, {