bhooai-nexus 2.0.13 → 3.0.4

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 (198) hide show
  1. package/README.md +113 -43
  2. package/package.json +23 -14
  3. package/packages/nexus-admin/package.json +1 -1
  4. package/packages/nexus-admin/src/App.tsx +280 -37
  5. package/packages/nexus-admin/src/api.ts +56 -0
  6. package/packages/nexus-admin/src/index.css +10 -1
  7. package/packages/nexus-ads/package.json +1 -1
  8. package/packages/nexus-ai-client/package.json +22 -20
  9. package/packages/nexus-ai-client/src/client.ts +3 -0
  10. package/packages/nexus-auth/package.json +1 -1
  11. package/packages/nexus-cache/package.json +1 -1
  12. package/packages/nexus-cli/package.json +6 -3
  13. package/packages/nexus-cli/src/commands/add.ts +32 -10
  14. package/packages/nexus-cli/src/commands/dev.ts +206 -127
  15. package/packages/nexus-cli/src/commands/init.ts +45 -12
  16. package/packages/nexus-cli/src/commands/license.ts +174 -0
  17. package/packages/nexus-cli/src/devPanel.ts +1381 -990
  18. package/packages/nexus-cli/src/devServiceManager.ts +416 -40
  19. package/packages/nexus-cli/src/features.ts +58 -11
  20. package/packages/nexus-cli/src/fusion/server.ts +251 -0
  21. package/packages/nexus-cli/src/index.ts +2 -0
  22. package/packages/nexus-cli/src/launcher.ts +35 -3
  23. package/packages/nexus-cli/src/ports.ts +112 -61
  24. package/packages/nexus-cli/src/tui.ts +272 -15
  25. package/packages/nexus-cli/src/wizard.ts +210 -23
  26. package/packages/nexus-cli/templates/base/apps/admin/vite.config.ts.ejs +15 -9
  27. package/packages/nexus-cli/templates/base/apps/backend/package.json.ejs +1 -0
  28. package/packages/nexus-cli/templates/base/apps/backend/src/live/root.live.js.ejs +26 -0
  29. package/packages/nexus-cli/templates/base/apps/backend/src/main.ts +1 -1
  30. package/packages/nexus-cli/templates/base/apps/backend/src/routes/index.ts.ejs +2 -0
  31. package/packages/nexus-cli/templates/base/apps/backend/src/routes/upload.ts.ejs +31 -0
  32. package/packages/nexus-cli/templates/base/apps/frontend/index.html.ejs +1 -0
  33. package/packages/nexus-cli/templates/base/apps/frontend/src/App.tsx.ejs +7 -2
  34. package/packages/nexus-cli/templates/base/apps/frontend/src/NotFound.tsx.ejs +56 -0
  35. package/packages/nexus-cli/templates/base/apps/frontend/vite.config.ts.ejs +15 -8
  36. package/packages/nexus-cli/templates/base/apps/pyserver/main.py.ejs +19 -0
  37. package/packages/nexus-cli/templates/base/apps/pyserver/requirements.txt.ejs +20 -0
  38. package/packages/nexus-cli/templates/base/apps/pyserver/routers/.gitkeep +0 -0
  39. package/packages/nexus-cli/templates/base/apps/pyserver/scripts/chat.py.ejs +40 -0
  40. package/packages/nexus-cli/templates/base/apps/pyserver/scripts/hello.py.ejs +5 -0
  41. package/packages/nexus-cli/templates/base/dockerignore +0 -1
  42. package/packages/nexus-cli/templates/base/gitignore +4 -1
  43. package/packages/nexus-cli/templates/base/nexus.config.ts.ejs +37 -1
  44. package/packages/nexus-cli/templates/base/package.json.ejs +12 -11
  45. package/packages/nexus-core/package.json +3 -2
  46. package/packages/nexus-core/src/app/Storage.ts +616 -9
  47. package/packages/nexus-core/src/app/Upload.ts +114 -0
  48. package/packages/nexus-core/src/app/adminModule.ts +256 -52
  49. package/packages/nexus-core/src/app/authModule.ts +56 -30
  50. package/packages/nexus-core/src/app/createNexusApp.ts +171 -28
  51. package/packages/nexus-core/src/app/databaseAdminModule.ts +171 -2
  52. package/packages/nexus-core/src/app/discover.ts +4 -2
  53. package/packages/nexus-core/src/app/errorPages.ts +21 -5
  54. package/packages/nexus-core/src/app/fusionEngine.ts +53 -0
  55. package/packages/nexus-core/src/app/index.ts +3 -0
  56. package/packages/nexus-core/src/app/preflightModule.ts +41 -4
  57. package/packages/nexus-core/src/app/userStore.ts +321 -0
  58. package/packages/nexus-core/src/certs/generator.ts +224 -0
  59. package/packages/nexus-core/src/config/dbAccess.ts +26 -0
  60. package/packages/nexus-core/src/config/defaults.ts +34 -4
  61. package/packages/nexus-core/src/config/index.ts +1 -0
  62. package/packages/nexus-core/src/config/schema.ts +72 -6
  63. package/packages/nexus-core/src/config/types.ts +94 -1
  64. package/packages/nexus-core/src/http/Server.ts +27 -3
  65. package/packages/nexus-core/src/http/static.ts +1 -1
  66. package/packages/nexus-crypto/package.json +1 -1
  67. package/packages/nexus-crypto/src/index.ts +2 -1
  68. package/packages/nexus-crypto/src/license.ts +141 -0
  69. package/packages/nexus-data/package.json +1 -1
  70. package/packages/nexus-data/src/errors.ts +11 -3
  71. package/packages/nexus-data/src/model/Model.ts +25 -1
  72. package/packages/nexus-data/src/schema/Schema.ts +1 -1
  73. package/packages/nexus-email/package.json +1 -1
  74. package/packages/nexus-examples/examples/chat/apps/backend/src/ws/chat.room.ts +11 -0
  75. package/packages/nexus-examples/examples/emotion-garden/apps/backend/src/live/_nav.js +17 -0
  76. package/packages/nexus-examples/examples/emotion-garden/apps/backend/src/live/garden.live.js +105 -0
  77. package/packages/nexus-examples/examples/file-storage/README.md +22 -4
  78. package/packages/nexus-examples/examples/file-storage/apps/backend/src/graphql/app.graph.ts +60 -2
  79. package/packages/nexus-examples/examples/file-storage/apps/backend/src/handlers/file.handler.ts +164 -6
  80. package/packages/nexus-examples/examples/file-storage/apps/frontend/src/main.tsx +32 -7
  81. package/packages/nexus-examples/examples/fusion-tree/README.md +169 -0
  82. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/handlers/ai.handler.ts +368 -0
  83. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/handlers/cache.handler.ts +79 -0
  84. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/handlers/fusion.handler.ts +437 -0
  85. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/routes/cache.ts +21 -0
  86. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/routes/fusion.ts +40 -0
  87. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/services/AiService.ts +184 -0
  88. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/services/CacheService.ts +65 -0
  89. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/services/FusionService.ts +141 -0
  90. package/packages/nexus-examples/examples/fusion-tree/apps/backend/src/services/QueryCache.ts +169 -0
  91. package/packages/nexus-examples/examples/fusion-tree/apps/frontend/src/AiPanel.tsx +179 -0
  92. package/packages/nexus-examples/examples/fusion-tree/apps/frontend/src/CachePanel.tsx +114 -0
  93. package/packages/nexus-examples/examples/fusion-tree/apps/frontend/src/main.tsx +412 -0
  94. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/_nav.js +29 -0
  95. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/ai.live.js +84 -0
  96. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/all-examples.live.js +42 -0
  97. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/chat.live.js +32 -0
  98. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/counter.live.js +24 -0
  99. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/gallery.live.js +46 -0
  100. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/posts.live.js +32 -0
  101. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/presence.live.js +47 -0
  102. package/packages/nexus-examples/examples/future-more/apps/backend/src/live/stats.live.js +50 -0
  103. package/packages/nexus-examples/examples/image-lab/README.md +26 -0
  104. package/packages/nexus-examples/examples/image-lab/apps/backend/src/routes/images.ts +184 -0
  105. package/packages/nexus-examples/examples/image-lab/apps/frontend/src/App.tsx +235 -0
  106. package/packages/nexus-examples/examples/image-lab/apps/frontend/src/main.tsx +6 -0
  107. package/packages/nexus-examples/examples/mario-platformer/README.md +13 -0
  108. package/packages/nexus-examples/examples/mario-platformer/apps/backend/src/live/_nav.js +16 -0
  109. package/packages/nexus-examples/examples/mario-platformer/apps/backend/src/live/root.live.js +178 -0
  110. package/packages/nexus-examples/examples/morph-ui/apps/backend/src/live/_nav.js +19 -0
  111. package/packages/nexus-examples/examples/morph-ui/apps/backend/src/live/cards.live.js +58 -0
  112. package/packages/nexus-examples/examples/morph-ui/apps/backend/src/live/list.live.js +84 -0
  113. package/packages/nexus-examples/examples/morph-ui/apps/backend/src/live/morphui.live.js +189 -0
  114. package/packages/nexus-examples/examples/python-bridge/README.md +12 -0
  115. package/packages/nexus-examples/examples/python-bridge/_setup.mjs +27 -0
  116. package/packages/nexus-examples/examples/python-bridge/apps/backend/src/routes/python-bridge.ts +120 -0
  117. package/packages/nexus-examples/examples/python-bridge/apps/frontend/src/App.tsx +61 -0
  118. package/packages/nexus-examples/examples/python-bridge-future/README.md +10 -0
  119. package/packages/nexus-examples/examples/python-bridge-future/_setup.mjs +27 -0
  120. package/packages/nexus-examples/examples/python-bridge-future/apps/backend/src/live/_nav.js +16 -0
  121. package/packages/nexus-examples/examples/python-bridge-future/apps/backend/src/live/p.live.js +12 -0
  122. package/packages/nexus-examples/examples/python-bridge-future/apps/backend/src/live/p1.live.js +13 -0
  123. package/packages/nexus-examples/examples/python-bridge-future/apps/backend/src/live/root.live.js +53 -0
  124. package/packages/nexus-examples/examples/saas-starter/apps/backend/src/graphql/app.graph.ts +4 -3
  125. package/packages/nexus-examples/examples/saas-starter/apps/frontend/src/main.tsx +6 -3
  126. package/packages/nexus-examples/package.json +1 -1
  127. package/packages/nexus-examples/src/index.ts +84 -1
  128. package/packages/nexus-fusion/package.json +41 -0
  129. package/packages/nexus-fusion/src/adapters/MongoMirror.ts +115 -0
  130. package/packages/nexus-fusion/src/adapters/RedisBridge.ts +89 -0
  131. package/packages/nexus-fusion/src/adapters/index.ts +3 -0
  132. package/packages/nexus-fusion/src/cache/FusionCache.ts +422 -0
  133. package/packages/nexus-fusion/src/cache/index.ts +2 -0
  134. package/packages/nexus-fusion/src/firebase/offlineQueue.ts +89 -0
  135. package/packages/nexus-fusion/src/firebase/onSnapshot.ts +98 -0
  136. package/packages/nexus-fusion/src/firebase/refs.ts +422 -0
  137. package/packages/nexus-fusion/src/firebase/securityRules.ts +121 -0
  138. package/packages/nexus-fusion/src/firebase/transforms.ts +73 -0
  139. package/packages/nexus-fusion/src/firebase/types.ts +45 -0
  140. package/packages/nexus-fusion/src/index.ts +248 -0
  141. package/packages/nexus-fusion/src/native/index.d.ts +0 -0
  142. package/packages/nexus-fusion/src/native/loader.ts +168 -0
  143. package/packages/nexus-fusion/src/playground/playgroundHtml.ts +461 -0
  144. package/packages/nexus-fusion/src/realtime/Emitter.ts +30 -0
  145. package/packages/nexus-fusion/src/schemas/auth.ts +32 -0
  146. package/packages/nexus-fusion/src/schemas/cms.ts +40 -0
  147. package/packages/nexus-fusion/src/schemas/core.ts +155 -0
  148. package/packages/nexus-fusion/src/schemas/ecommerce.ts +46 -0
  149. package/packages/nexus-fusion/src/schemas/index.ts +19 -0
  150. package/packages/nexus-fusion-core/package.json +20 -0
  151. package/packages/nexus-fusion-core/src/cache.rs +196 -0
  152. package/packages/nexus-fusion-core/src/engine.rs +772 -0
  153. package/packages/nexus-fusion-core/src/errors.rs +51 -0
  154. package/packages/nexus-fusion-core/src/idgen.rs +100 -0
  155. package/packages/nexus-fusion-core/src/lib.rs +17 -0
  156. package/packages/nexus-fusion-core/src/napi_glue.rs +333 -0
  157. package/packages/nexus-fusion-core/src/query.rs +513 -0
  158. package/packages/nexus-fusion-core/src/tree.rs +282 -0
  159. package/packages/nexus-fusion-core/src/types.rs +147 -0
  160. package/packages/nexus-fusion-core/src/wal.rs +1877 -0
  161. package/packages/nexus-future/LICENSE +21 -0
  162. package/packages/nexus-future/package.json +63 -0
  163. package/packages/nexus-future/src/ai/autoui.js +229 -0
  164. package/packages/nexus-future/src/ai/crud.js +144 -0
  165. package/packages/nexus-future/src/ai/fields.js +141 -0
  166. package/packages/nexus-future/src/ai/http.js +180 -0
  167. package/packages/nexus-future/src/ai/index.js +132 -0
  168. package/packages/nexus-future/src/ai/mock.js +14 -0
  169. package/packages/nexus-future/src/html/index.js +22 -0
  170. package/packages/nexus-future/src/html/signals.js +191 -0
  171. package/packages/nexus-future/src/html/template.js +459 -0
  172. package/packages/nexus-future/src/index.js +50 -0
  173. package/packages/nexus-future/src/live/client.js +580 -0
  174. package/packages/nexus-future/src/live/index.js +5 -0
  175. package/packages/nexus-future/src/live/morph.js +198 -0
  176. package/packages/nexus-future/src/live/rpc.js +53 -0
  177. package/packages/nexus-future/src/live/server.js +464 -0
  178. package/packages/nexus-future/src/live/stream.js +104 -0
  179. package/packages/nexus-future/src/page/index.js +180 -0
  180. package/packages/nexus-future/src/server/mount.js +192 -0
  181. package/packages/nexus-future/src/ui/canvas.js +142 -0
  182. package/packages/nexus-future/src/ui/index.js +2 -0
  183. package/packages/nexus-future/src/ui/sense.js +233 -0
  184. package/packages/nexus-graphql/package.json +1 -1
  185. package/packages/nexus-payments/package.json +23 -22
  186. package/packages/nexus-payments/src/index.ts +4 -0
  187. package/packages/nexus-plugins/package.json +1 -1
  188. package/packages/nexus-postcss/package.json +1 -1
  189. package/packages/nexus-realtime/package.json +1 -1
  190. package/packages/nexus-safe-goto/package.json +1 -1
  191. package/packages/nexus-telemetry/package.json +1 -1
  192. package/src/index.js +34 -0
  193. package/docs/ARCHITECTURE.md +0 -169
  194. package/docs/CLI.md +0 -147
  195. package/docs/CONVENTIONS.md +0 -243
  196. package/docs/MULTI_APP.md +0 -107
  197. package/docs/STORAGE.md +0 -118
  198. package/src/index.ts +0 -53
@@ -12,14 +12,17 @@ import {
12
12
  getPaymentOrders, getPaymentTransactions, getPaymentStatus, createPaymentOrder, updatePaymentProvider, savePaymentProviderKeys,
13
13
  getDatabases, createDatabase, deleteDatabase,
14
14
  createCollection, modifyCollection, dropCollection, getCollectionDocs,
15
+ getUploads, getUploadStats, getUploadIndex, rebuildUploadIndex, reindexUploads,
15
16
  generateSchema, getAiStatus, getAiModels, aiChat, aiChatStream, getAiProviders, updateAiProvider, addAiProvider, deleteAiProvider, testAiProvider,
16
17
  getAllLogs, clearLogs, type LogEntry,
17
18
  getRequestLogs, getRequestSeries, type RequestLogEntry, type RequestSeries, type RequestSeriesRange,
18
19
  type ServiceState, type AdminConfig, type AdminEnv, type EnvEntry, type DatabaseInfo, type GeneratedSchema, type PaymentProviderStatus, type PaymentProviderField, type AiStatus, type AiModelInfo, type AiChatMessage, type AiProviderView, type AiProviderPersistence, type AiProviderTestResult, type UserRecord, type RoleDefinition, type PreflightReport, type PreflightCheck, type LintReport,
20
+ getCertStatus, generateCerts, type CertStatus, type CertGenerateResult,
21
+ type UploadRecord, type UploadsResponse, type UploadStats, type UploadIndexStatus,
19
22
  } from './api.js';
20
23
 
21
24
 
22
- type Tab = 'overview' | 'processes' | 'logs' | 'config' | 'env' | 'plugins' | 'users' | 'monitoring' | 'payments' | 'databases' | 'schema' | 'ai' | 'graphql' | 'theme';
25
+ type Tab = 'overview' | 'processes' | 'logs' | 'config' | 'env' | 'plugins' | 'users' | 'monitoring' | 'payments' | 'databases' | 'uploads' | 'schema' | 'ai' | 'graphql' | 'theme';
23
26
  type AdminTheme = 'aurora' | 'midnight' | 'violet';
24
27
  type UiMode = 'workspace' | 'compact' | 'focus';
25
28
  type ToggleStyle = 'chip' | 'neon' | 'icon' | 'orb' | 'track';
@@ -457,6 +460,7 @@ function Dashboard({ user, onUserChange, onLogout }: { user: AdminUser | null; o
457
460
  { id: 'monitoring', label: 'Monitoring', icon: 'activity', keywords: ['metrics', 'telemetry', 'health', 'uptime', 'traffic', 'stats'], desc: 'Live backend metrics and request traffic.' },
458
461
  { id: 'payments', label: 'Payments', icon: 'dollar', keywords: ['orders', 'transactions', 'money', 'ledger', 'checkout', 'razorpay', 'paypal', 'stripe'], desc: 'Orders, transactions and provider status.' },
459
462
  { id: 'databases', label: 'Databases', icon: 'database', keywords: ['mongo', 'mongodb', 'collections', 'data', 'tables', 'docs'], desc: 'Databases, collections and their documents.' },
463
+ { id: 'uploads', label: 'Uploads', icon: 'image', keywords: ['uploads', 'files', 'images', 'search', 'index', 'storage', 'sha256', 'variants', 'dedup'], desc: 'Millions-scale uploads, search index and dedup.' },
460
464
  { id: 'schema', label: 'AI Schema', icon: 'sparkles', keywords: ['generate', 'ai', 'model', 'json schema', 'validator'], desc: 'Generate MongoDB schemas from plain English.' },
461
465
  { id: 'ai', label: 'AI Agents', icon: 'bot', keywords: ['providers', 'chat', 'playground', 'ollama', 'openai', 'models', 'settings'], desc: 'AI settings, providers and chat playground.' },
462
466
  { id: 'graphql', label: 'GraphQL', icon: 'gitFork', keywords: ['graphiql', 'explorer', 'schema', 'playground', 'graphql', 'api'], desc: 'GraphQL explorer and access control.' },
@@ -644,6 +648,7 @@ function Dashboard({ user, onUserChange, onLogout }: { user: AdminUser | null; o
644
648
  {tab === 'monitoring' && <Monitoring />}
645
649
  {tab === 'payments' && <Payments toggle={customTheme.toggle} />}
646
650
  {tab === 'databases' && <Databases />}
651
+ {tab === 'uploads' && <Uploads />}
647
652
  {tab === 'schema' && <Schema />}
648
653
  {tab === 'ai' && <AiAgents toggle={customTheme.toggle} />}
649
654
  {tab === 'graphql' && <GraphQLSettings onNavigate={setTab} />}
@@ -1459,13 +1464,13 @@ function Overview({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
1459
1464
  <div className="overview-hero"><div><span className="admin-overline">GOOD MORNING, ADMIN</span><h1>Your project is <em>in orbit.</em></h1><p>One calm surface for the services, configuration, data, and AI that power your Nexus application.</p></div><div className="hero-orbit-art"><span /><i /><b /><strong>NX</strong></div></div>
1460
1465
  <div className="overview-stat-grid"><div className="overview-stat"><span>ACTIVE SERVICES</span><strong>{running}<small>/{services.length || 4}</small></strong><b className="stat-good">● running now</b></div><div className="overview-stat"><span>HTTP REQUESTS</span><strong>{requestCount || '—'}</strong><b>since boot</b></div><div className="overview-stat"><span>UPTIME</span><strong>{metrics ? `${Math.round((metrics.uptime ?? 0) / 60)}m` : '—'}</strong><b>backend process</b></div><div className="overview-stat"><span>ENVIRONMENT</span><strong>DEV</strong><b className="stat-violet">local workspace</b></div></div>
1461
1466
  <section className="workspace-panel live-traffic-panel"><div className="panel-title-row"><div><span className="admin-overline">HTTP REQUESTS</span><h3>Live traffic</h3></div><span className="live-badge"><i className="status-dot good" /> LIVE</span></div><TrafficChart samples={traffic} total={requestCount} last={traffic.length ? traffic[traffic.length - 1] : 0} onNavigate={onNavigate} /></section>
1462
- <div className="overview-columns"><section className="workspace-panel"><div className="panel-title-row"><div><span className="admin-overline">PROJECT SURFACES</span><h3>Everything in one place</h3></div><button onClick={() => onNavigate('config')} className="text-action">View config →</button></div><div className="surface-grid"><button className="surface-card acc-blue" onClick={() => onNavigate('config')}><span className="surface-icon"><Icon name="braces" size={20} /></span><strong>Runtime config</strong><small>nexus.runtime.json overrides</small><i className="surface-go">→</i></button><button className="surface-card acc-violet" onClick={() => onNavigate('env')}><span className="surface-icon"><Icon name="key" size={20} /></span><strong>Environment</strong><small>Masked secrets editor</small><i className="surface-go">→</i></button><button className="surface-card acc-pink" onClick={() => onNavigate('databases')}><span className="surface-icon"><Icon name="database" size={20} /></span><strong>Data layer</strong><small>Mongo databases &amp; collections</small><i className="surface-go">→</i></button><button className="surface-card acc-blue" onClick={() => onNavigate('theme')}><span className="surface-icon"><Icon name="sparkles" size={20} /></span><strong>AI theme</strong><small>Generate schemas from English</small><i className="surface-go">→</i></button><button className="surface-card acc-violet" onClick={() => onNavigate('users')}><span className="surface-icon"><Icon name="users" size={20} /></span><strong>Users &amp; roles</strong><small>Accounts, roles &amp; grants</small><i className="surface-go">→</i></button><button className="surface-card acc-pink" onClick={() => onNavigate('payments')}><span className="surface-icon"><Icon name="dollar" size={20} /></span><strong>Payments</strong><small>Orders, transactions &amp; providers</small><i className="surface-go">→</i></button><button className="surface-card acc-blue" onClick={() => onNavigate('ai')}><span className="surface-icon"><Icon name="bot" size={20} /></span><strong>AI agents</strong><small>Providers &amp; chat playground</small><i className="surface-go">→</i></button></div></section><section className="workspace-panel pulse-panel"><div className="panel-title-row"><div><span className="admin-overline">SYSTEM PULSE</span><h3>Services are moving</h3></div><span className="live-badge"><i className="status-dot good" /> LIVE</span></div><div className="pulse-bars">{[34,55,42,78,62,88,52,72,48,66,84,58].map((height, index) => <i key={index} style={{ height: `${height}%` }} />)}</div><p>Live supervisor status refreshes every five seconds.</p><button onClick={() => onNavigate('monitoring')} className="text-action">Open monitoring →</button></section></div>
1467
+ <div className="overview-columns"><section className="workspace-panel"><div className="panel-title-row"><div><span className="admin-overline">PROJECT SURFACES</span><h3>Everything in one place</h3></div><button onClick={() => onNavigate('config')} className="text-action">View config →</button></div><div className="surface-grid"><button className="surface-card acc-blue" onClick={() => onNavigate('config')}><span className="surface-icon"><Icon name="braces" size={20} /></span><strong>Runtime config</strong><small>nexus.runtime.json overrides</small><i className="surface-go">→</i></button><button className="surface-card acc-violet" onClick={() => onNavigate('uploads')}><span className="surface-icon"><Icon name="image" size={20} /></span><strong>Uploads</strong><small>Searchable file index</small><i className="surface-go">→</i></button><button className="surface-card acc-pink" onClick={() => onNavigate('databases')}><span className="surface-icon"><Icon name="database" size={20} /></span><strong>Data layer</strong><small>Mongo databases &amp; collections</small><i className="surface-go">→</i></button><button className="surface-card acc-blue" onClick={() => onNavigate('theme')}><span className="surface-icon"><Icon name="sparkles" size={20} /></span><strong>AI theme</strong><small>Generate schemas from English</small><i className="surface-go">→</i></button><button className="surface-card acc-violet" onClick={() => onNavigate('users')}><span className="surface-icon"><Icon name="users" size={20} /></span><strong>Users &amp; roles</strong><small>Accounts, roles &amp; grants</small><i className="surface-go">→</i></button><button className="surface-card acc-pink" onClick={() => onNavigate('payments')}><span className="surface-icon"><Icon name="dollar" size={20} /></span><strong>Payments</strong><small>Orders, transactions &amp; providers</small><i className="surface-go">→</i></button><button className="surface-card acc-blue" onClick={() => onNavigate('ai')}><span className="surface-icon"><Icon name="bot" size={20} /></span><strong>AI agents</strong><small>Providers &amp; chat playground</small><i className="surface-go">→</i></button></div></section><section className="workspace-panel pulse-panel"><div className="panel-title-row"><div><span className="admin-overline">SYSTEM PULSE</span><h3>Services are moving</h3></div><span className="live-badge"><i className="status-dot good" /> LIVE</span></div><div className="pulse-bars">{[34,55,42,78,62,88,52,72,48,66,84,58].map((height, index) => <i key={index} style={{ height: `${height}%` }} />)}</div><p>Live supervisor status refreshes every five seconds.</p><button onClick={() => onNavigate('monitoring')} className="text-action">Open monitoring →</button></section></div>
1463
1468
  <section className="workspace-panel">
1464
1469
  <div className="panel-title-row">
1465
- <div><span className="admin-overline">PREFLIGHT DIAGNOSTICS</span><h3>Dependencies are being probed</h3></div>
1466
- <button onClick={runChecks} disabled={preflightBusy} className="glass-chip-btn">{preflightBusy ? 'probing…' : <><Icon name="refresh" size={12} /> run checks</>}</button>
1470
+ <div><span className="admin-overline">PREFLIGHT DIAGNOSTICS</span><h3>{preflightBusy ? 'Probing dependencies…' : preflight ? 'Dependency health' : 'Dependency diagnostics'}</h3></div>
1471
+ <button onClick={runChecks} disabled={preflightBusy} className="glass-chip-btn">{preflightBusy ? 'Probing…' : <><Icon name="refresh" size={12} /> Run checks</>}</button>
1467
1472
  </div>
1468
- <p className="preflight-note">The Python server pings the backend API, AI server, GraphQL endpoint and data-store ports, and reports latency per target.</p>
1473
+ <p className="preflight-note">The Node backend probes the API, AI server, GraphQL endpoint, data-store ports (Mongo/Redis), enabled AI providers, the Fusion DB server, the Python pyserver, SMTP relay and the frontend dev server, and reports latency per target.</p>
1469
1474
  {preflightErr && (
1470
1475
  <div role="alert" className="mb-3 rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-sm text-rose-400">
1471
1476
  Preflight failed — {preflightErr}
@@ -1562,7 +1567,7 @@ function Processes() {
1562
1567
  return (
1563
1568
  <div className="space-y-6">
1564
1569
  <PageHead title="Processes">
1565
- <button onClick={refresh} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh</button>
1570
+ <button onClick={refresh} className="glass-chip-btn"><Icon name="refresh" size={12} /> Refresh</button>
1566
1571
  </PageHead>
1567
1572
  <PageHero kicker="SERVICE CONTROL PLANE" title={<>Your services, <em>in check.</em></>} desc="Every backend, frontend and AI process the supervisor is tending — live status, uptime, PID and one-click control." glyph="server" art="bars" />
1568
1573
  <div className="glass-panel overflow-hidden p-2">
@@ -1582,10 +1587,10 @@ function Processes() {
1582
1587
  <td className="text-center text-slate-400">{s.startedAt ? `${Math.round((Date.now() - s.startedAt) / 1000)}s` : '—'}</td>
1583
1588
  <td className="text-center">
1584
1589
  <span className="inline-flex gap-1.5">
1585
- <button onClick={() => controlService('start', s.name)} className="glass-chip-btn">start</button>
1586
- <button onClick={() => controlService('stop', s.name)} className="glass-chip-btn-danger">stop</button>
1587
- <button onClick={() => controlService('restart', s.name)} className="glass-chip-btn">restart</button>
1588
- <button onClick={() => showLogs(s.name)} className="glass-chip-btn">logs</button>
1590
+ <button onClick={() => controlService('start', s.name)} className="glass-chip-btn">Start</button>
1591
+ <button onClick={() => controlService('stop', s.name)} className="glass-chip-btn-danger">Stop</button>
1592
+ <button onClick={() => controlService('restart', s.name)} className="glass-chip-btn">Restart</button>
1593
+ <button onClick={() => showLogs(s.name)} className="glass-chip-btn">Logs</button>
1589
1594
  </span>
1590
1595
  </td>
1591
1596
  </tr>
@@ -1775,10 +1780,10 @@ function Logs({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
1775
1780
  <PageHead title="Logs">
1776
1781
  <div className="flex flex-wrap items-center gap-2">
1777
1782
  <button aria-pressed={live} onClick={() => setLive(!live)} className={`glass-chip-btn inline-flex items-center gap-1.5 log-live-btn${live ? ' is-live' : ''}`}>{live ? '● live' : '○ paused'}</button>
1778
- <button aria-label="Refresh logs" onClick={() => void refresh()} className="glass-chip-btn inline-flex items-center gap-1.5"><Icon name="refresh" size={12} /> refresh</button>
1779
- <button aria-label="Copy filtered logs" onClick={() => void copyAll()} className="glass-chip-btn inline-flex items-center gap-1.5">{copied ? 'Copied' : <><Icon name="copy" size={12} /> copy</>}</button>
1780
- <button aria-label="Summarize logs with AI" onClick={() => void runAiSummary()} disabled={aiSummaryBusy} className="glass-chip-btn log-ai-btn inline-flex items-center gap-1.5"><Icon name="sparkles" size={12} /> {aiSummaryBusy ? 'summarizing…' : 'ai summary'}</button>
1781
- <button aria-label="Clear all logs" onClick={async () => { try { await clearLogs(); await refresh(); } catch { /* supervisor may not be reachable */ } }} className="glass-chip-btn-danger inline-flex items-center gap-1.5"><Icon name="trash" size={12} /> clear</button>
1783
+ <button aria-label="Refresh logs" onClick={() => void refresh()} className="glass-chip-btn inline-flex items-center gap-1.5"><Icon name="refresh" size={12} /> Refresh</button>
1784
+ <button aria-label="Copy filtered logs" onClick={() => void copyAll()} className="glass-chip-btn inline-flex items-center gap-1.5">{copied ? 'Copied' : <><Icon name="copy" size={12} /> Copy</>}</button>
1785
+ <button aria-label="Summarize logs with AI" onClick={() => void runAiSummary()} disabled={aiSummaryBusy} className="glass-chip-btn log-ai-btn inline-flex items-center gap-1.5"><Icon name="sparkles" size={12} /> {aiSummaryBusy ? 'Summarizing…' : 'AI summary'}</button>
1786
+ <button aria-label="Clear all logs" onClick={async () => { try { await clearLogs(); await refresh(); } catch { /* supervisor may not be reachable */ } }} className="glass-chip-btn-danger inline-flex items-center gap-1.5"><Icon name="trash" size={12} /> Clear</button>
1782
1787
  </div>
1783
1788
  </PageHead>
1784
1789
  <PageHero kicker="ERROR CONSOLE" title={<>Every failure, <em>in one stream.</em></>} desc="Aggregated stdout, stderr and supervisor events from every service — live as the supervisor records them, filterable and searchable." glyph="terminal" art="wave" />
@@ -1816,9 +1821,9 @@ function Logs({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
1816
1821
  <div className="log-ai-head">
1817
1822
  <span className="log-ai-title"><Icon name="sparkles" size={12} /> AI summary{aiModel ? ` · ${aiModel}` : ''}</span>
1818
1823
  <div className="log-ai-actions">
1819
- {aiSummaryBusy && <button onClick={stopAiSummary} className="glass-chip-btn-danger"><Icon name="square" size={10} /> stop</button>}
1820
- {!aiSummaryBusy && aiSummary && <button onClick={() => void copyAiSummary()} className="glass-chip-btn"><Icon name="copy" size={12} /> copy</button>}
1821
- <button onClick={() => { setAiSummaryOpen(false); setAiSummary(''); setAiSummaryErr(''); }} className="glass-chip-btn"><Icon name="x" size={12} /> dismiss</button>
1824
+ {aiSummaryBusy && <button onClick={stopAiSummary} className="glass-chip-btn-danger"><Icon name="square" size={10} /> Stop</button>}
1825
+ {!aiSummaryBusy && aiSummary && <button onClick={() => void copyAiSummary()} className="glass-chip-btn"><Icon name="copy" size={12} /> Copy</button>}
1826
+ <button onClick={() => { setAiSummaryOpen(false); setAiSummary(''); setAiSummaryErr(''); }} className="glass-chip-btn"><Icon name="x" size={12} /> Dismiss</button>
1822
1827
  </div>
1823
1828
  </div>
1824
1829
  {aiSummaryErr && (
@@ -1944,7 +1949,7 @@ function LintChecks({ run, title, overline }: { run: () => Promise<LintReport>;
1944
1949
  <section className="workspace-panel lint-panel">
1945
1950
  <div className="panel-title-row">
1946
1951
  <div><span className="admin-overline">{overline}</span><h3>{title}</h3></div>
1947
- <button onClick={doRun} disabled={busy} className="glass-chip-btn">{busy ? 'linting…' : <><Icon name="refresh" size={12} /> run lint</>}</button>
1952
+ <button onClick={doRun} disabled={busy} className="glass-chip-btn">{busy ? 'Linting…' : <><Icon name="refresh" size={12} /> Run lint</>}</button>
1948
1953
  </div>
1949
1954
  {err && <p className="preflight-empty">{err}</p>}
1950
1955
  {report ? (
@@ -2050,7 +2055,7 @@ function AiSettings() {
2050
2055
  <section className="glass-card space-y-3">
2051
2056
  <div className="flex items-center justify-between">
2052
2057
  <h3 className="glass-h3">AI server health</h3>
2053
- <button onClick={checkAi} className="glass-chip-btn">re-check</button>
2058
+ <button onClick={checkAi} className="glass-chip-btn">Re-check</button>
2054
2059
  </div>
2055
2060
  {aiErr && <p className="text-xs text-rose-400">{aiErr}</p>}
2056
2061
  <div className="grid grid-cols-1 gap-3 md:grid-cols-3">
@@ -2354,7 +2359,7 @@ function AiProviders({ toggle }: { toggle: ToggleStyle }) {
2354
2359
  <div className="flex items-center justify-between">
2355
2360
  <h3 className="glass-h3">AI providers ({providers.length})</h3>
2356
2361
  <div className="flex gap-2">
2357
- <button onClick={loadProviders} disabled={busy} className="glass-chip-btn">refresh</button>
2362
+ <button onClick={loadProviders} disabled={busy} className="glass-chip-btn">Refresh</button>
2358
2363
  <button onClick={() => setShowAdd(true)} disabled={busy} className="glass-chip-btn">+ add provider</button>
2359
2364
  </div>
2360
2365
  </div>
@@ -2607,7 +2612,7 @@ function ProviderCard({ provider, toggle, anim, busy, isCustom, testing, testRes
2607
2612
  {/* Test row */}
2608
2613
  <div className="flex items-center gap-2 pt-0.5 border-t border-white/[0.06]">
2609
2614
  <button type="button" onClick={onTest} disabled={busy || testing} className="glass-chip-btn shrink-0 text-[10px]">
2610
- {testing ? 'testing…' : <><Icon name="zap" size={11} /> test</>}
2615
+ {testing ? 'Testing…' : <><Icon name="zap" size={11} /> Test</>}
2611
2616
  </button>
2612
2617
  {testResult && (
2613
2618
  testResult.ok ? (
@@ -2969,8 +2974,8 @@ function AiChatPlayground() {
2969
2974
  <div className="flex items-center justify-between">
2970
2975
  <h3 className="glass-h3">Chat playground</h3>
2971
2976
  <div className="flex gap-2">
2972
- <button onClick={checkAi} disabled={streaming} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh models</button>
2973
- <button onClick={clearChat} disabled={streaming} className="glass-chip-btn">clear</button>
2977
+ <button onClick={checkAi} disabled={streaming} className="glass-chip-btn"><Icon name="refresh" size={12} /> Refresh models</button>
2978
+ <button onClick={clearChat} disabled={streaming} className="glass-chip-btn">Clear</button>
2974
2979
  </div>
2975
2980
  </div>
2976
2981
 
@@ -3064,6 +3069,13 @@ function Config({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
3064
3069
  const [newKey, setNewKey] = useState('');
3065
3070
  const [newType, setNewType] = useState<ConfigValueType>('string');
3066
3071
  const [newValue, setNewValue] = useState('');
3072
+ // HTTP/HTTPS transport switch — backed by server.https + certFile/keyFile
3073
+ const [serverHttps, setServerHttps] = useState(false);
3074
+ const [serverCertFile, setServerCertFile] = useState('');
3075
+ const [serverKeyFile, setServerKeyFile] = useState('');
3076
+ const [certStatus, setCertStatus] = useState<CertStatus | null>(null);
3077
+ const [certBusy, setCertBusy] = useState(false);
3078
+ const [certMsg, setCertMsg] = useState('');
3067
3079
 
3068
3080
  const addKey = () => {
3069
3081
  if (!newKey.trim()) return;
@@ -3071,6 +3083,32 @@ function Config({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
3071
3083
  setNewKey(''); setNewType('string'); setNewValue('');
3072
3084
  };
3073
3085
 
3086
+ const refreshCertStatus = async () => {
3087
+ try { setCertStatus(await getCertStatus()); } catch { setCertStatus(null); }
3088
+ };
3089
+
3090
+ const handleGenerateCerts = async (force = false) => {
3091
+ setCertBusy(true); setCertMsg('');
3092
+ try {
3093
+ const res = await generateCerts({ force });
3094
+ if (res.ok) {
3095
+ setCertStatus(res.status ?? null);
3096
+ if (res.certFile) setServerCertFile(res.certFile);
3097
+ if (res.keyFile) setServerKeyFile(res.keyFile);
3098
+ setCertMsg(`Generated ${res.method} certificates: ${res.certFile} + ${res.keyFile}${res.overwritten ? ' (overwritten)' : ''}. Saved to runtime — restart to use.`);
3099
+ } else {
3100
+ setCertMsg(`Failed: ${(res as unknown as { reason?: string }).reason ?? 'unknown error'}`);
3101
+ }
3102
+ } catch (e: any) {
3103
+ const msg = String(e?.message ?? e);
3104
+ if (msg.includes('already exist')) {
3105
+ setCertMsg('Certificates already exist. Tick “overwrite” to regenerate.');
3106
+ } else {
3107
+ setCertMsg(`Error: ${msg}`);
3108
+ }
3109
+ } finally { setCertBusy(false); }
3110
+ };
3111
+
3074
3112
  const load = async () => {
3075
3113
  try {
3076
3114
  const c = await getAdminConfig();
@@ -3083,10 +3121,17 @@ function Config({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
3083
3121
  const base = overrides.length
3084
3122
  ? overrides
3085
3123
  : flattenConfig(c.config).filter((entry) => entry.value !== '***');
3086
- setEntries(base);
3124
+ // Extract transport keys into dedicated switch state; keep the table free of duplicates
3125
+ const s = (c.config?.server ?? {}) as { https?: boolean; certFile?: string; keyFile?: string };
3126
+ setServerHttps(!!s.https);
3127
+ setServerCertFile((s.certFile as string) ?? '');
3128
+ setServerKeyFile((s.keyFile as string) ?? '');
3129
+ const filtered = base.filter((e) => !['server.https', 'server.certFile', 'server.keyFile'].includes(e.key));
3130
+ setEntries(filtered);
3087
3131
  setFilePath(c.file?.path ?? '');
3088
3132
  setFileText(c.file?.content ?? '');
3089
3133
  setMsg(c.file ? '' : 'No human-edited config file found.');
3134
+ void refreshCertStatus();
3090
3135
  } catch (e: any) { setMsg(String(e?.message ?? e)); }
3091
3136
  };
3092
3137
  useEffect(() => { load(); }, []);
@@ -3094,7 +3139,20 @@ function Config({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
3094
3139
  const saveRuntime = async () => {
3095
3140
  setBusy(true); setMsg('');
3096
3141
  try {
3097
- const overrides = buildConfig(entries);
3142
+ const baseConfig = buildConfig(entries);
3143
+ const serverPatch: Record<string, unknown> = { https: serverHttps };
3144
+ if (serverHttps) {
3145
+ if (serverCertFile.trim()) serverPatch.certFile = serverCertFile.trim();
3146
+ if (serverKeyFile.trim()) serverPatch.keyFile = serverKeyFile.trim();
3147
+ } else {
3148
+ // Keep cert paths in runtime if the user already typed them — the server ignores them when https is false
3149
+ if (serverCertFile.trim()) serverPatch.certFile = serverCertFile.trim();
3150
+ if (serverKeyFile.trim()) serverPatch.keyFile = serverKeyFile.trim();
3151
+ }
3152
+ const overrides = {
3153
+ ...baseConfig,
3154
+ server: { ...((baseConfig.server as Record<string, unknown>) ?? {}), ...serverPatch },
3155
+ } as Record<string, unknown>;
3098
3156
  await putAdminConfig(overrides);
3099
3157
  setMsg('Saved to nexus.runtime.json. Restart services to apply changes.');
3100
3158
  setRestartDialog(true);
@@ -3115,6 +3173,79 @@ function Config({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
3115
3173
  <PageHead title="Configuration"><div className="heading-actions"><span className="restart-badge">RESTART REQUIRED AFTER SAVE</span><button onClick={load} className="workspace-action"><Icon name="refresh" size={12} /> Reload</button></div></PageHead>
3116
3174
  <div className="config-intro"><div><span className="admin-overline">PROJECT CONTROL PLANE</span><h1>Configure your <em>runtime.</em></h1><p>Use key/value rows for safe JSON overrides, or switch to source mode for the typed project configuration.</p></div><div className="hero-art hero-art--cubes"><strong>{'{ }'}</strong><span className="ha-a" /><span className="ha-b" /><span className="ha-c" /></div></div>
3117
3175
  <div className="workspace-tabs">{([['runtime', 'Runtime JSON'], ['source', 'Source file'], ['effective', 'Effective view']] as const).map(([id, label]) => <button key={id} onClick={() => setMode(id)} className={mode === id ? 'is-active' : ''}>{label}</button>)}</div>
3176
+ {mode === 'runtime' && (
3177
+ <section className="glass-card space-y-3">
3178
+ <div className="flex items-center justify-between">
3179
+ <div>
3180
+ <span className="admin-overline">TRANSPORT · PROTOCOL</span>
3181
+ <h3 className="glass-h3">HTTP / HTTPS switch</h3>
3182
+ </div>
3183
+ <span className={serverHttps ? 'provider-badge-ok' : 'provider-badge-mute'}>{serverHttps ? 'https' : 'http'}</span>
3184
+ </div>
3185
+ <p className="text-xs text-slate-400">Toggle the backend protocol. HTTPS requires certificate and key files. Saved to <code className="font-mono text-slate-300">nexus.runtime.json</code> as <code className="font-mono text-slate-300">server.https</code> (+ certFile/keyFile). Restart required after save.</p>
3186
+ <div className="flex flex-wrap items-center gap-3">
3187
+ <div className="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 p-1">
3188
+ <button type="button" onClick={() => setServerHttps(false)} className={`rounded-full px-4 py-1.5 text-xs font-medium transition-colors ${!serverHttps ? 'bg-white text-slate-900 shadow' : 'text-slate-400 hover:text-slate-200'}`}>HTTP</button>
3189
+ <button type="button" onClick={() => setServerHttps(true)} className={`rounded-full px-4 py-1.5 text-xs font-medium transition-colors ${serverHttps ? 'bg-emerald-500 text-white shadow' : 'text-slate-400 hover:text-slate-200'}`}>HTTPS</button>
3190
+ </div>
3191
+ <button type="button" onClick={() => setServerHttps(!serverHttps)} className={`ai-toggle ${serverHttps ? 'is-on' : ''}`} aria-label="Toggle HTTPS" title={serverHttps ? 'HTTPS enabled — click for HTTP' : 'HTTP enabled — click for HTTPS'}>
3192
+ <span className="ai-toggle-knob" />
3193
+ </button>
3194
+ <span className="text-xs text-slate-500">{serverHttps ? 'Backend will listen with TLS' : 'Backend will listen plain HTTP'}</span>
3195
+ </div>
3196
+ {serverHttps && (
3197
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
3198
+ <label className="text-xs text-slate-300">Cert file path
3199
+ <input value={serverCertFile} onChange={(e) => setServerCertFile(e.target.value)} placeholder="certs/server.crt" className="glass-input mt-1 block w-full font-mono text-xs" />
3200
+ </label>
3201
+ <label className="text-xs text-slate-300">Key file path
3202
+ <input value={serverKeyFile} onChange={(e) => setServerKeyFile(e.target.value)} placeholder="certs/server.key" className="glass-input mt-1 block w-full font-mono text-xs" />
3203
+ </label>
3204
+ </div>
3205
+ )}
3206
+ {serverHttps && (!serverCertFile.trim() || !serverKeyFile.trim()) && (
3207
+ <p className="rounded-lg border border-amber-400/30 bg-amber-400/10 px-3 py-2 text-xs text-amber-200">HTTPS is on but cert/key paths are empty. The server will fail to start without valid files — generate self-signed certs into <code className="font-mono">certs/</code> or set paths before restarting. You can still save and add keys later.</p>
3208
+ )}
3209
+ {!serverHttps && (serverCertFile || serverKeyFile) && (
3210
+ <p className="text-xs text-slate-500">Cert paths are kept but ignored while HTTP is selected. They will be used again when you switch back to HTTPS.</p>
3211
+ )}
3212
+ <div className="flex items-center gap-2 text-xs text-slate-500">
3213
+ <span>Effective URL:</span>
3214
+ <code className="font-mono text-slate-300">{`${serverHttps ? 'https' : 'http'}://${((cfg?.config as unknown as { server?: { host?: string; port?: number } })?.server?.host) ?? 'localhost'}:${((cfg?.config as unknown as { server?: { port?: number } })?.server?.port) ?? 4000}`}</code>
3215
+ </div>
3216
+
3217
+ {/* Self-signed certificate generation */}
3218
+ <div className="rounded-xl border border-white/10 bg-white/[0.03] p-3">
3219
+ <div className="flex flex-wrap items-center justify-between gap-2">
3220
+ <div>
3221
+ <span className="admin-overline">CERTIFICATES</span>
3222
+ <div className="text-xs text-slate-200">Self-signed certificate</div>
3223
+ {certStatus ? (
3224
+ <div className="mt-1 flex flex-wrap items-center gap-2 text-[11px] font-mono">
3225
+ <span className={certStatus.certExists ? 'text-emerald-400' : 'text-rose-400'}>cert: {certStatus.certFile} {certStatus.certExists ? '● exists' : '○ missing'}{certStatus.certInfo ? ` (${Math.round((certStatus.certInfo.size)/1024*10)/10}k)` : ''}</span>
3226
+ <span className={certStatus.keyExists ? 'text-emerald-400' : 'text-rose-400'}>key: {certStatus.keyFile} {certStatus.keyExists ? '● exists' : '○ missing'}{certStatus.keyInfo ? ` (${Math.round((certStatus.keyInfo.size)/1024*10)/10}k)` : ''}</span>
3227
+ {certStatus.ready ? <span className="provider-badge-ok">ready</span> : <span className="provider-badge-warn">not ready</span>}
3228
+ </div>
3229
+ ) : (
3230
+ <div className="text-[11px] text-slate-500">Checking…</div>
3231
+ )}
3232
+ </div>
3233
+ <div className="flex items-center gap-2">
3234
+ <button type="button" onClick={() => void refreshCertStatus()} className="glass-chip-btn" disabled={certBusy}><Icon name="refresh" size={12} /> Check</button>
3235
+ <button type="button" onClick={() => void handleGenerateCerts(false)} disabled={certBusy} className="glass-chip-btn-primary" title="Generate server.crt + server.key in certs/">{certBusy ? 'Generating…' : <><Icon name="sparkles" size={12} /> Generate cert</>}</button>
3236
+ </div>
3237
+ </div>
3238
+ {certStatus?.ready && (
3239
+ <div className="mt-2 flex items-center gap-2">
3240
+ <span className="text-[11px] text-slate-500">Regenerate (overwrite existing files):</span>
3241
+ <button type="button" onClick={() => void handleGenerateCerts(true)} disabled={certBusy} className="glass-chip-btn-danger text-[11px]">{certBusy ? '…' : 'Overwrite' }</button>
3242
+ </div>
3243
+ )}
3244
+ {certMsg && <p className={`mt-2 rounded-lg px-3 py-2 text-xs ${certMsg.startsWith('Generated') ? 'border border-emerald-400/30 bg-emerald-400/10 text-emerald-200' : certMsg.startsWith('Failed') || certMsg.startsWith('Error') ? 'border border-rose-400/30 bg-rose-400/10 text-rose-300' : 'border border-white/10 bg-white/5 text-slate-300'}`}>{certMsg}</p>}
3245
+ <p className="mt-2 text-[11px] text-slate-500">Generates a self-signed RSA 2048 certificate for <code className="font-mono">localhost</code> (+ 127.0.0.1) valid 365 days via OpenSSL or pure-JS fallback. Paths are auto-saved to <code className="font-mono">nexus.runtime.json</code> as <code className="font-mono">server.certFile</code>/<code className="font-mono">server.keyFile</code>.</p>
3246
+ </div>
3247
+ </section>
3248
+ )}
3118
3249
  {mode === 'runtime' && <section className="workspace-panel config-panel"><div className="panel-title-row"><div><span className="admin-overline">NEXUS CONFIG · KEY/VALUE</span><h3>Configuration overrides</h3></div><button onClick={addKey} className="workspace-action">+ Add key</button></div><p className="panel-description">Existing configuration is loaded into the rows below. Changes are written to nexus.runtime.json and validated against the Nexus schema before saving. Secrets are redacted and managed in the Environment surface.</p><div className="key-add-bar">
3119
3250
  <input value={newKey} onChange={(event) => setNewKey(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addKey(); }} placeholder="key path — e.g. server.port" />
3120
3251
  <select value={newType} onChange={(event) => { setNewType(event.target.value as ConfigValueType); if (event.target.value === 'null') setNewValue(''); }}><option value="string">string</option><option value="number">number</option><option value="boolean">boolean</option><option value="null">null</option><option value="object">object</option><option value="array">array</option></select>
@@ -3337,8 +3468,7 @@ function GraphQLSettings({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
3337
3468
  {requireAuth && <p className="text-xs text-amber-400">When enabled, opening /graphiql requires an active admin session (same guard as /admin/*).</p>}
3338
3469
  {msg && <p className="text-xs text-slate-400">{msg}</p>}
3339
3470
  <div className="flex items-center justify-end gap-2">
3340
- <button onClick={load} className="glass-chip-btn"><Icon name="refresh" size={12} /> Reload</button>
3341
- <button onClick={save} disabled={busy} className="glass-chip-btn-primary shrink-0">{busy ? 'Saving…' : 'Save'} <span>→</span></button>
3471
+ <button onClick={load} className="glass-chip-btn"><Icon name="refresh" size={12} /> Reload</button> <button onClick={save} disabled={busy} className="glass-chip-btn-primary shrink-0">{busy ? 'Saving…' : 'Save'} <span>→</span></button>
3342
3472
  </div>
3343
3473
  </section>
3344
3474
 
@@ -3703,7 +3833,7 @@ function Payments({ toggle }: { toggle: ToggleStyle }) {
3703
3833
  <option value="all" className="bg-slate-900">All providers</option>
3704
3834
  {ALL_PROVIDERS.map((p) => <option key={p} value={p} className="bg-slate-900">{p}</option>)}
3705
3835
  </select>
3706
- <button onClick={() => load()} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh</button>
3836
+ <button onClick={() => load()} className="glass-chip-btn"><Icon name="refresh" size={12} /> Refresh</button>
3707
3837
  </div>
3708
3838
  </PageHead>
3709
3839
  <PageHero kicker="PAYMENT OPERATIONS" title={<>Money flow, <em>on the ledger.</em></>} desc="Every charge, refund and provider event from your payment stack — one surface to reconcile what went through." glyph="dollar" art="cards" />
@@ -3849,7 +3979,7 @@ function TestConsole({ toggle, status, onRun, onNotice, onCreated }: { toggle: T
3849
3979
  <div className="flex items-center justify-between">
3850
3980
  <h3 className="glass-h3">Provider status</h3>
3851
3981
  <button onClick={async () => { setProbeBusy(true); try { await onRun(); } finally { setProbeBusy(false); } }} className="glass-chip-btn" disabled={probeBusy}>
3852
- {probeBusy ? 'checking…' : <><Icon name="refresh" size={12} /> re-run checks</>}
3982
+ {probeBusy ? 'Checking…' : <><Icon name="refresh" size={12} /> Re-run checks</>}
3853
3983
  </button>
3854
3984
  </div>
3855
3985
  <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
@@ -3871,7 +4001,7 @@ function TestConsole({ toggle, status, onRun, onNotice, onCreated }: { toggle: T
3871
4001
  <div className="text-[11px] text-slate-500 font-mono truncate">{p.sandbox ? 'sandbox mode' : 'live mode'}</div>
3872
4002
  </div>
3873
4003
  <span className={p.ok ? 'dot-ok' : 'dot-bad'} title={p.ok ? 'working' : 'not working'} />
3874
- <button onClick={() => openKeys(p)} className="glass-chip-btn shrink-0 text-[10px]" title="Set API key / secret"><Icon name="key" size={11} /> keys</button>
4004
+ <button onClick={() => openKeys(p)} className="glass-chip-btn shrink-0 text-[10px]" title="Set API key / secret"><Icon name="key" size={11} /> Keys</button>
3875
4005
  </div>
3876
4006
  <div className="flex flex-wrap items-center gap-1.5">
3877
4007
  {p.enabled
@@ -4063,7 +4193,7 @@ function Databases() {
4063
4193
  return (
4064
4194
  <div className="space-y-6">
4065
4195
  <PageHead title="Databases">
4066
- <button onClick={refresh} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh</button>
4196
+ <button onClick={refresh} className="glass-chip-btn"><Icon name="refresh" size={12} /> Refresh</button>
4067
4197
  </PageHead>
4068
4198
  <PageHero kicker="DATA FOUNDATION" title={<>Your data, <em>organized.</em></>} desc="Databases and collections backing your project — inspect, create and keep the layer that stores everything sane." glyph="database" art="cubes" />
4069
4199
 
@@ -4080,24 +4210,24 @@ function Databases() {
4080
4210
  <span className={`mr-1 inline-block transition-transform ${expanded === db.name ? 'rotate-90' : ''}`}><Icon name="chevronRight" size={14} /></span>{db.name}
4081
4211
  </button>
4082
4212
  <span className="text-xs text-slate-500">{(db.sizeOnDisk / 1024).toFixed(1)} KB · {db.collections.length} collections</span>
4083
- <button onClick={() => setConfirm({ title: `Drop database “${db.name}”?`, message: <>This will permanently delete the database <strong>{db.name}</strong> and all of its collections. This cannot be undone.</>, action: async () => { if (await withOk(() => deleteDatabase(db.name))) await refresh(); } })} className="glass-chip-btn-danger ml-auto">drop</button>
4213
+ <button onClick={() => setConfirm({ title: `Drop database “${db.name}”?`, message: <>This will permanently delete the database <strong>{db.name}</strong> and all of its collections. This cannot be undone.</>, action: async () => { if (await withOk(() => deleteDatabase(db.name))) await refresh(); } })} className="glass-chip-btn-danger ml-auto">Drop</button>
4084
4214
  </div>
4085
4215
 
4086
4216
  {expanded === db.name && (
4087
4217
  <div className="mt-4 space-y-2.5">
4088
4218
  <div className="flex gap-2">
4089
4219
  <input className="glass-input max-w-xs !py-1.5 text-sm" placeholder="new collection" value={newColl[db.name] ?? ''} onChange={(e) => setNewColl({ ...newColl, [db.name]: e.target.value })} />
4090
- <button onClick={() => doCreateColl(db.name)} className="glass-chip-btn">create</button>
4220
+ <button onClick={() => doCreateColl(db.name)} className="glass-chip-btn">Create</button>
4091
4221
  </div>
4092
4222
  {db.collections.length === 0 && <p className="text-sm text-slate-500">No collections.</p>}
4093
4223
  {db.collections.map((c) => (
4094
4224
  <div key={`${db.name}/${c.name}`} className="flex flex-wrap items-center gap-2 border-t border-white/[0.06] pt-2.5 text-sm">
4095
4225
  <span className="font-mono text-slate-200">{c.name}</span>
4096
4226
  <span className="text-xs text-slate-500">{c.count} docs</span>
4097
- <button onClick={() => toggleDocs(db.name, c.name)} className="glass-chip-btn ml-auto">docs</button>
4227
+ <button onClick={() => toggleDocs(db.name, c.name)} className="glass-chip-btn ml-auto">Docs</button>
4098
4228
  <input className="glass-input w-36 !py-1 text-xs" placeholder="rename to…" value={renames[`${db.name}/${c.name}`] ?? ''} onChange={(e) => setRenames({ ...renames, [`${db.name}/${c.name}`]: e.target.value })} />
4099
- <button onClick={() => doRename(db.name, c.name)} className="glass-chip-btn">rename</button>
4100
- <button onClick={() => setConfirm({ title: `Drop collection “${c.name}”?`, message: <>This will permanently delete the collection <strong>{db.name}/{c.name}</strong> and its {c.count} document{c.count === 1 ? '' : 's'}. This cannot be undone.</>, action: async () => { if (await withOk(() => dropCollection(db.name, c.name))) await refresh(); } })} className="glass-chip-btn-danger">drop</button>
4229
+ <button onClick={() => doRename(db.name, c.name)} className="glass-chip-btn">Rename</button>
4230
+ <button onClick={() => setConfirm({ title: `Drop collection “${c.name}”?`, message: <>This will permanently delete the collection <strong>{db.name}/{c.name}</strong> and its {c.count} document{c.count === 1 ? '' : 's'}. This cannot be undone.</>, action: async () => { if (await withOk(() => dropCollection(db.name, c.name))) await refresh(); } })} className="glass-chip-btn-danger">Drop</button>
4101
4231
  </div>
4102
4232
  ))}
4103
4233
  </div>
@@ -4130,6 +4260,119 @@ function Databases() {
4130
4260
  );
4131
4261
  }
4132
4262
 
4263
+ function Uploads() {
4264
+ const [q, setQ] = useState('');
4265
+ const [mime, setMime] = useState('');
4266
+ const [page, setPage] = useState(1);
4267
+ const [sort, setSort] = useState<'recent' | 'oldest' | 'size' | 'name'>('recent');
4268
+ const [data, setData] = useState<UploadsResponse | null>(null);
4269
+ const [stats, setStats] = useState<UploadStats | null>(null);
4270
+ const [index, setIndex] = useState<UploadIndexStatus | null>(null);
4271
+ const [busy, setBusy] = useState(false);
4272
+ const [msg, setMsg] = useAdminAlert('uploads');
4273
+
4274
+ const load = async (p = page) => {
4275
+ setBusy(true);
4276
+ try {
4277
+ const res = await getUploads({ q: q.trim() || undefined, mime: mime.trim() || undefined, page: p, limit: 20, sort });
4278
+ setData(res);
4279
+ setPage(p);
4280
+ if (!res.ok) setMsg(res.reason ?? 'Failed to load uploads');
4281
+ else setMsg('');
4282
+ } catch (e: any) {
4283
+ setMsg(String(e?.message ?? e));
4284
+ } finally {
4285
+ setBusy(false);
4286
+ }
4287
+ };
4288
+ const loadStats = async () => {
4289
+ try {
4290
+ setStats(await getUploadStats());
4291
+ setIndex(await getUploadIndex());
4292
+ } catch {}
4293
+ };
4294
+ useEffect(() => { load(1); loadStats(); }, []);
4295
+ // Debounced search
4296
+ useEffect(() => {
4297
+ const t = setTimeout(() => { load(1); }, 300);
4298
+ return () => clearTimeout(t);
4299
+ }, [q, mime, sort]);
4300
+
4301
+ const rebuild = async () => {
4302
+ setBusy(true);
4303
+ setMsg('Rebuilding search index…');
4304
+ try {
4305
+ const res = await rebuildUploadIndex();
4306
+ setMsg(res.ok ? 'Index rebuilt' : `Rebuild failed: ${res.reason ?? 'unknown'}`);
4307
+ setIndex(await getUploadIndex());
4308
+ await load(page);
4309
+ } catch (e: any) {
4310
+ setMsg(String(e?.message ?? e));
4311
+ } finally {
4312
+ setBusy(false);
4313
+ }
4314
+ };
4315
+
4316
+ return (
4317
+ <div className="space-y-6">
4318
+ <PageHead title="Uploads">
4319
+ <button onClick={() => { load(page); loadStats(); }} className="glass-chip-btn"><Icon name="refresh" size={12} /> Refresh</button>
4320
+ <button onClick={rebuild} disabled={busy} className="glass-chip-btn">{busy ? '…' : 'Rebuild index'}</button>
4321
+ </PageHead>
4322
+ <PageHero kicker="STORAGE" title={<>Uploads, <em>searchable.</em></>} desc="Single DB entry per sha256, placements per directory, symlink dedup — millions searchable via text index." glyph="image" art="cubes" />
4323
+
4324
+ {stats && (
4325
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-4">
4326
+ <div className="glass-card"><div className="text-xs text-slate-500">Total uploads (unique sha256)</div><div className="text-2xl font-bold text-slate-100">{stats.total}</div></div>
4327
+ <div className="glass-card"><div className="text-xs text-slate-500">Total bytes</div><div className="text-2xl font-bold text-slate-100">{(stats.totalSize / (1024 * 1024)).toFixed(2)} MB</div></div>
4328
+ <div className="glass-card"><div className="text-xs text-slate-500">MIME breakdown</div><div className="text-xs text-slate-300">{stats.mimeBreakdown.slice(0, 3).map((m) => `${m._id}:${m.count}`).join(' · ') || '—'}</div></div>
4329
+ <div className="glass-card"><div className="text-xs text-slate-500">Search index</div><div className={`text-xs ${index?.hasText ? 'text-emerald-400' : 'text-amber-400'}`}>{index?.hasText ? 'text index ✓' : 'no text index'} · {index?.indexes?.length ?? 0} indexes</div><div className="text-[0.6rem] text-slate-500">{index?.hasSha256 ? 'sha256 ✓' : 'sha256 —'}</div></div>
4330
+ </div>
4331
+ )}
4332
+
4333
+ <div className="glass-card flex flex-wrap gap-2">
4334
+ <div className="flex items-center gap-2">
4335
+ <Icon name="search" size={14} />
4336
+ <input className="glass-input w-64" placeholder="search name, mime, sha256, dir" value={q} onChange={(e) => setQ(e.target.value)} />
4337
+ </div>
4338
+ <input className="glass-input w-32" placeholder="mime filter" value={mime} onChange={(e) => setMime(e.target.value)} />
4339
+ <select className="glass-input w-32" value={sort} onChange={(e) => setSort(e.target.value as any)}>
4340
+ <option value="recent">recent</option><option value="oldest">oldest</option><option value="size">size</option><option value="name">name</option>
4341
+ </select>
4342
+ <span className="text-xs text-slate-500">{data?.total ?? 0} total · page {data?.page ?? 1}/{data?.pages ?? 1}</span>
4343
+ </div>
4344
+
4345
+ <div className="glass-card overflow-auto">
4346
+ <table className="w-full text-left text-sm">
4347
+ <thead className="text-xs text-slate-500"><tr><th className="p-2">Preview</th><th className="p-2">Name</th><th className="p-2">MIME</th><th className="p-2">Size</th><th className="p-2">Placements</th><th className="p-2">Variants</th><th className="p-2">Created</th></tr></thead>
4348
+ <tbody>
4349
+ {data?.uploads?.map((u) => (
4350
+ <tr key={u.sha256} className="border-t border-white/5">
4351
+ <td className="p-2">{u.mime.startsWith('image/') ? <img src={u.url} alt={u.originalName} className="h-10 w-10 rounded object-cover" onError={(e) => ((e.currentTarget.style.display = 'none'))} /> : <span className="text-xs text-slate-500">—</span>}</td>
4352
+ <td className="p-2"><div className="font-mono text-xs text-slate-200">{u.originalName}</div><div className="text-[0.6rem] text-slate-500">{u.sha256.slice(0, 12)}…</div></td>
4353
+ <td className="p-2 text-xs text-slate-400">{u.mime}</td>
4354
+ <td className="p-2 text-xs text-slate-400">{(u.size / 1024).toFixed(1)} KB</td>
4355
+ <td className="p-2 text-xs text-slate-400">{u.placementsCount} dirs<br /><span className="text-[0.6rem]">{u.placements?.slice(0, 2).map((p) => p.dir).join(', ')}</span></td>
4356
+ <td className="p-2 text-xs text-slate-400">{u.variants ? Object.keys(u.variants).join(', ') : '—'}</td>
4357
+ <td className="p-2 text-xs text-slate-500">{u.createdAt ? new Date(u.createdAt).toLocaleString() : '—'}</td>
4358
+ </tr>
4359
+ ))}
4360
+ {data && data.uploads.length === 0 && <tr><td colSpan={7} className="p-4 text-center text-sm text-slate-500">No uploads match “{q}”</td></tr>}
4361
+ {!data && <tr><td colSpan={7} className="p-4 text-center text-sm text-slate-500">Loading…</td></tr>}
4362
+ </tbody>
4363
+ </table>
4364
+ </div>
4365
+
4366
+ <div className="flex items-center gap-2">
4367
+ <button disabled={page <= 1 || busy} onClick={() => load(page - 1)} className="glass-chip-btn">Prev</button>
4368
+ <span className="text-xs text-slate-500">page {page} / {data?.pages ?? 1}</span>
4369
+ <button disabled={data ? page >= (data.pages ?? 1) : true || busy} onClick={() => load(page + 1)} className="glass-chip-btn">Next</button>
4370
+ <span className="ml-auto text-xs text-slate-500">Per-backend storage: apps/backend/storage, single DB entry per sha256, symlink across dirs.</span>
4371
+ </div>
4372
+ </div>
4373
+ );
4374
+ }
4375
+
4133
4376
  function Schema() {
4134
4377
  const [prompt, setPrompt] = useState('');
4135
4378
  const [databases, setDatabases] = useState<DatabaseInfo[]>([]);
@@ -4182,7 +4425,7 @@ function Schema() {
4182
4425
  <section className="glass-card space-y-3">
4183
4426
  <div className="flex items-center justify-between">
4184
4427
  <h3 className="glass-h3">AI provider status</h3>
4185
- <button onClick={checkAi} className="glass-chip-btn">re-check</button>
4428
+ <button onClick={checkAi} className="glass-chip-btn">Re-check</button>
4186
4429
  </div>
4187
4430
  {aiErr && <p className="text-xs text-rose-400">{aiErr}</p>}
4188
4431
  <div className="grid grid-cols-1 gap-3 md:grid-cols-3">