underpost 3.2.8 → 3.2.9
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.
- package/CHANGELOG.md +102 -2
- package/CLI-HELP.md +16 -2
- package/README.md +3 -3
- package/bin/build.js +1 -2
- package/bin/file.js +1 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +17 -17
- package/scripts/k3s-node-setup.sh +2 -2
- package/scripts/nat-iptables.sh +103 -18
- package/src/cli/cluster.js +61 -14
- package/src/cli/db.js +47 -2
- package/src/cli/deploy.js +36 -7
- package/src/cli/fs.js +17 -3
- package/src/cli/index.js +21 -0
- package/src/cli/repository.js +34 -28
- package/src/cli/run.js +149 -35
- package/src/client/components/core/Auth.js +15 -5
- package/src/client/components/core/Docs.js +6 -34
- package/src/client/components/core/FileExplorer.js +6 -6
- package/src/client/components/core/Modal.js +65 -2
- package/src/client/components/core/Recover.js +4 -4
- package/src/client/components/core/Worker.js +48 -27
- package/src/client/services/default/default.management.js +20 -25
- package/src/client/services/user/guest.service.js +10 -3
- package/src/index.js +1 -1
- package/src/server/data-query.js +32 -20
- package/src/server/dns.js +22 -0
- package/src/server/start.js +14 -3
- package/src/server/valkey.js +9 -2
- package/typedoc.json +10 -1
package/src/cli/run.js
CHANGED
|
@@ -110,6 +110,10 @@ const logger = loggerFactory(import.meta);
|
|
|
110
110
|
* @property {string|Array<{ip: string, hostnames: string[]}>} hostAliases - Adds entries to the Pod /etc/hosts via Kubernetes hostAliases.
|
|
111
111
|
* As a string (CLI): semicolon-separated entries of "ip=hostname1,hostname2" (e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote").
|
|
112
112
|
* As an array (programmatic): objects with `ip` and `hostnames` fields (e.g., [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]).
|
|
113
|
+
* @property {boolean} gitClean - Whether to perform a `git clean` before running.
|
|
114
|
+
* @property {boolean} copy - Whether to copy the command to the clipboard instead of executing it.
|
|
115
|
+
* @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
|
|
116
|
+
* @property {boolean} pullBundle - Whether to pull the bundle before running. Use together with --skip-full-build to skip the local build entirely (supported by: sync, template-deploy).
|
|
113
117
|
* @memberof UnderpostRun
|
|
114
118
|
*/
|
|
115
119
|
const DEFAULT_OPTION = {
|
|
@@ -174,6 +178,8 @@ const DEFAULT_OPTION = {
|
|
|
174
178
|
hostAliases: '',
|
|
175
179
|
gitClean: false,
|
|
176
180
|
copy: false,
|
|
181
|
+
skipFullBuild: false,
|
|
182
|
+
pullBundle: false,
|
|
177
183
|
};
|
|
178
184
|
|
|
179
185
|
/**
|
|
@@ -435,6 +441,15 @@ class UnderpostRun {
|
|
|
435
441
|
deployType = 'init';
|
|
436
442
|
}
|
|
437
443
|
|
|
444
|
+
// If --build is set and path is a sync-engine-* target, push the pre-built client bundle
|
|
445
|
+
// to Cloudinary so the remote container can pull it instead of rebuilding from source.
|
|
446
|
+
if (options.build && deployConfId && deployConfId.startsWith('engine-')) {
|
|
447
|
+
const confName = deployConfId.replace(/^engine-/, '');
|
|
448
|
+
const pushDeployId = options.deployId || `dd-${confName}`;
|
|
449
|
+
logger.info(`[template-deploy] Running push-bundle for deployId: ${pushDeployId}`);
|
|
450
|
+
shellExec(`${baseCommand} run push-bundle --deploy-id ${pushDeployId}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
438
453
|
// Dispatch npmpkg CI workflow — this builds pwa-microservices-template first.
|
|
439
454
|
// If deployConfId is set, npmpkg.ci.yml will dispatch the engine-<conf-id> CI
|
|
440
455
|
// with sync=true after template build completes. The engine CI then dispatches
|
|
@@ -683,12 +698,15 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
683
698
|
: '';
|
|
684
699
|
const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
|
|
685
700
|
|
|
701
|
+
const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
|
|
702
|
+
const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
|
|
703
|
+
|
|
686
704
|
shellExec(
|
|
687
705
|
`${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
|
|
688
706
|
image ? ` --image ${image}` : ''
|
|
689
707
|
}${versions ? ` --versions ${versions}` : ''}${
|
|
690
708
|
options.namespace ? ` --namespace ${options.namespace}` : ''
|
|
691
|
-
}${timeoutFlags}${cmdString}${gitCleanFlag} ${deployId} ${env}`,
|
|
709
|
+
}${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag} ${deployId} ${env}`,
|
|
692
710
|
);
|
|
693
711
|
|
|
694
712
|
if (isDeployRunnerContext(path, options)) {
|
|
@@ -1288,6 +1306,44 @@ EOF
|
|
|
1288
1306
|
shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
|
|
1289
1307
|
},
|
|
1290
1308
|
|
|
1309
|
+
/**
|
|
1310
|
+
* @method install-crio
|
|
1311
|
+
* @description Installs and configures CRI-O as the container runtime for kubeadm clusters.
|
|
1312
|
+
* Adds the stable v1.33 CRI-O yum repository, installs the `cri-o` package, configures
|
|
1313
|
+
* the systemd cgroup driver, enables the `crio` service, and writes `/etc/crictl.yaml`
|
|
1314
|
+
* so that `crictl` targets the CRI-O socket by default.
|
|
1315
|
+
* @param {string} path - Unused.
|
|
1316
|
+
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
1317
|
+
* @memberof UnderpostRun
|
|
1318
|
+
*/
|
|
1319
|
+
'install-crio': (path, options = DEFAULT_OPTION) => {
|
|
1320
|
+
logger.info('Installing CRI-O...');
|
|
1321
|
+
shellExec(`cat <<EOF | sudo tee /etc/yum.repos.d/cri-o.repo
|
|
1322
|
+
[cri-o]
|
|
1323
|
+
name=CRI-O
|
|
1324
|
+
baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/
|
|
1325
|
+
enabled=1
|
|
1326
|
+
gpgcheck=1
|
|
1327
|
+
gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/repodata/repomd.xml.key
|
|
1328
|
+
EOF`);
|
|
1329
|
+
shellExec(`sudo dnf -y install cri-o`);
|
|
1330
|
+
// crictl is in the kubernetes repo but excluded by default — install it explicitly
|
|
1331
|
+
shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes`);
|
|
1332
|
+
// Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
|
|
1333
|
+
shellExec(
|
|
1334
|
+
`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf 2>/dev/null || true`,
|
|
1335
|
+
);
|
|
1336
|
+
shellExec(`sudo systemctl enable --now crio`);
|
|
1337
|
+
logger.info('CRI-O installed and started.');
|
|
1338
|
+
// Write crictl config so all crictl calls default to the CRI-O socket
|
|
1339
|
+
shellExec(`cat <<EOF | sudo tee /etc/crictl.yaml
|
|
1340
|
+
runtime-endpoint: unix:///var/run/crio/crio.sock
|
|
1341
|
+
image-endpoint: unix:///var/run/crio/crio.sock
|
|
1342
|
+
timeout: 10
|
|
1343
|
+
debug: false
|
|
1344
|
+
EOF`);
|
|
1345
|
+
},
|
|
1346
|
+
|
|
1291
1347
|
/**
|
|
1292
1348
|
* @method dd-container
|
|
1293
1349
|
* @description Deploys a development or debug container tasks jobs, setting up necessary volumes and images, and running specified commands within the container.
|
|
@@ -1430,6 +1486,9 @@ EOF
|
|
|
1430
1486
|
/**
|
|
1431
1487
|
* @method promote
|
|
1432
1488
|
* @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.router` for 'dd', or a specific ID).
|
|
1489
|
+
* When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
|
|
1490
|
+
* TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
|
|
1491
|
+
* (cert-manager Certificate resources) for each affected deployment.
|
|
1433
1492
|
* @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `deployId,env,replicas`).
|
|
1434
1493
|
* @param {Object} options - The default underpost runner options for customizing workflow
|
|
1435
1494
|
* @memberof UnderpostRun
|
|
@@ -1438,11 +1497,34 @@ EOF
|
|
|
1438
1497
|
let [inputDeployId, inputEnv, inputReplicas] = path.split(',');
|
|
1439
1498
|
if (!inputEnv) inputEnv = 'production';
|
|
1440
1499
|
if (!inputReplicas) inputReplicas = 1;
|
|
1500
|
+
// TODO: normalize: --tls maps to --cert for deploy.js isValidTLSContext compatibility
|
|
1501
|
+
if (options.tls) options.cert = true;
|
|
1502
|
+
|
|
1503
|
+
const applyCerts = (deployId, targetTraffic) => {
|
|
1504
|
+
if (!options.tls) return;
|
|
1505
|
+
// Rebuild proxy.yaml with --cert so the HTTPProxy includes TLS virtualhost config
|
|
1506
|
+
shellExec(
|
|
1507
|
+
`node bin deploy --build-manifest --cert --traffic ${targetTraffic} --replicas ${inputReplicas} --namespace ${options.namespace} ${deployId} ${inputEnv}`,
|
|
1508
|
+
);
|
|
1509
|
+
// Delete stale Certificate resources before reapplying
|
|
1510
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1511
|
+
if (fs.existsSync(confServerPath)) {
|
|
1512
|
+
for (const host of Object.keys(JSON.parse(fs.readFileSync(confServerPath, 'utf8'))))
|
|
1513
|
+
shellExec(`sudo kubectl delete Certificate ${host} -n ${options.namespace} --ignore-not-found`);
|
|
1514
|
+
}
|
|
1515
|
+
shellExec(
|
|
1516
|
+
`sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${inputEnv}/proxy.yaml -n ${options.namespace}`,
|
|
1517
|
+
);
|
|
1518
|
+
const secretPath = `./engine-private/conf/${deployId}/build/${inputEnv}/secret.yaml`;
|
|
1519
|
+
if (fs.existsSync(secretPath)) shellExec(`kubectl apply -f ${secretPath} -n ${options.namespace}`);
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1441
1522
|
if (inputDeployId === 'dd') {
|
|
1442
1523
|
for (const deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
|
|
1443
1524
|
const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
1444
1525
|
const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
|
|
1445
1526
|
Underpost.deploy.switchTraffic(deployId, inputEnv, targetTraffic, inputReplicas, options.namespace, options);
|
|
1527
|
+
applyCerts(deployId, targetTraffic);
|
|
1446
1528
|
}
|
|
1447
1529
|
} else {
|
|
1448
1530
|
const currentTraffic = Underpost.deploy.getCurrentTraffic(inputDeployId, { namespace: options.namespace });
|
|
@@ -1455,6 +1537,7 @@ EOF
|
|
|
1455
1537
|
options.namespace,
|
|
1456
1538
|
options,
|
|
1457
1539
|
);
|
|
1540
|
+
applyCerts(inputDeployId, targetTraffic);
|
|
1458
1541
|
}
|
|
1459
1542
|
},
|
|
1460
1543
|
/**
|
|
@@ -2371,9 +2454,11 @@ EOF`;
|
|
|
2371
2454
|
/**
|
|
2372
2455
|
* @method pull-bundle
|
|
2373
2456
|
* @description Downloads split zip parts from file storage, merges and extracts them, and moves the result into the public directory.
|
|
2374
|
-
* Steps: set
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2457
|
+
* Steps: set env, download parts (omit-unzip), merge zip, unzip, remove zip + parts, move to public/<host>[/path].
|
|
2458
|
+
* Iterates over every non-singleReplica, non-redirect, non-disabledRebuild route in conf.server.json
|
|
2459
|
+
* so that multi-path deployments are handled correctly.
|
|
2460
|
+
* @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
|
|
2461
|
+
* If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
|
|
2377
2462
|
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
2378
2463
|
* @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
|
|
2379
2464
|
* @param {boolean} [options.dev] - Use development environment; defaults to production.
|
|
@@ -2384,21 +2469,16 @@ EOF`;
|
|
|
2384
2469
|
const env = options.dev ? 'development' : 'production';
|
|
2385
2470
|
const deployId = options.deployId || 'dd-default';
|
|
2386
2471
|
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
2387
|
-
const
|
|
2472
|
+
const confServer = fs.existsSync(confServerPath) ? loadConfServerJson(confServerPath) : {};
|
|
2473
|
+
const hostsArg = path
|
|
2388
2474
|
? path
|
|
2389
2475
|
.split(',')
|
|
2390
2476
|
.map((h) => h.trim())
|
|
2391
2477
|
.filter(Boolean)
|
|
2392
|
-
:
|
|
2393
|
-
? Object.keys(loadConfServerJson(confServerPath))
|
|
2394
|
-
: [];
|
|
2478
|
+
: Object.keys(confServer);
|
|
2395
2479
|
|
|
2396
|
-
if (
|
|
2397
|
-
logger.error('pull-bundle: no hosts resolved', {
|
|
2398
|
-
deployId,
|
|
2399
|
-
path,
|
|
2400
|
-
confServerPath,
|
|
2401
|
-
});
|
|
2480
|
+
if (hostsArg.length === 0) {
|
|
2481
|
+
logger.error('pull-bundle: no hosts resolved', { deployId, path, confServerPath });
|
|
2402
2482
|
return;
|
|
2403
2483
|
}
|
|
2404
2484
|
|
|
@@ -2407,28 +2487,62 @@ EOF`;
|
|
|
2407
2487
|
shellExec(
|
|
2408
2488
|
`${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
|
|
2409
2489
|
);
|
|
2410
|
-
for (const host of hosts) {
|
|
2411
|
-
const zipPath = `build/${host}-.zip`;
|
|
2412
|
-
const hasZip = fs.existsSync(zipPath);
|
|
2413
|
-
const hasParts =
|
|
2414
|
-
fs.existsSync('./build') &&
|
|
2415
|
-
fs
|
|
2416
|
-
.readdirSync('./build')
|
|
2417
|
-
.some((name) => name.startsWith(`${host}-.zip.part`) || name.startsWith(`${host}-.zip-part`));
|
|
2418
|
-
|
|
2419
|
-
if (!hasZip && !hasParts) {
|
|
2420
|
-
logger.warn(`Bundle not found for host '${host}'. Skipping host.`, {
|
|
2421
|
-
zipPath,
|
|
2422
|
-
deployId,
|
|
2423
|
-
});
|
|
2424
|
-
continue;
|
|
2425
|
-
}
|
|
2426
2490
|
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2491
|
+
for (const host of hostsArg) {
|
|
2492
|
+
// Gather all routes for this host; fall back to root '/' when host is not in confServer
|
|
2493
|
+
// (e.g. when hosts were provided explicitly via the path argument).
|
|
2494
|
+
const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
|
|
2495
|
+
|
|
2496
|
+
for (const routePath of routePaths) {
|
|
2497
|
+
const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
|
|
2498
|
+
// Skip routes that are not built by buildClient (mirrors buildClient skip conditions)
|
|
2499
|
+
if (routeConf.singleReplica || routeConf.redirect || routeConf.disabledRebuild) continue;
|
|
2500
|
+
|
|
2501
|
+
// buildClient names the zip as "<host>-<path-no-slashes>.zip"
|
|
2502
|
+
// e.g. host="underpost.net", path="/" → buildId="underpost.net-", zip="build/underpost.net-.zip"
|
|
2503
|
+
// e.g. host="app.net", path="/admin" → buildId="app.net-admin", zip="build/app.net-admin.zip"
|
|
2504
|
+
const buildId = `${host}-${routePath.replaceAll('/', '')}`;
|
|
2505
|
+
const zipPath = `build/${buildId}.zip`;
|
|
2506
|
+
const buildDir = './build';
|
|
2507
|
+
const hasZip = fs.existsSync(zipPath);
|
|
2508
|
+
const hasParts =
|
|
2509
|
+
fs.existsSync(buildDir) &&
|
|
2510
|
+
fs
|
|
2511
|
+
.readdirSync(buildDir)
|
|
2512
|
+
.some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
|
|
2513
|
+
|
|
2514
|
+
if (!hasZip && !hasParts) {
|
|
2515
|
+
logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
|
|
2520
|
+
shellExec(`${baseCommand} client --unzip ${zipPath}`);
|
|
2521
|
+
shellExec(`sudo rm -rf ${zipPath}`);
|
|
2522
|
+
|
|
2523
|
+
// Clean up downloaded part wrapper zips left by --omit-unzip pull
|
|
2524
|
+
if (fs.existsSync(buildDir)) {
|
|
2525
|
+
fs.readdirSync(buildDir)
|
|
2526
|
+
.filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
|
|
2527
|
+
.forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// unzipClientBuild extracts to buildId with trailing '-' stripped
|
|
2531
|
+
// e.g. "build/underpost.net-" → "build/underpost.net"
|
|
2532
|
+
// e.g. "build/app.net-admin" → "build/app.net-admin" (no trailing dash, no change)
|
|
2533
|
+
const extractedDir = `build/${buildId.replace(/-$/, '')}`;
|
|
2534
|
+
if (!fs.existsSync(extractedDir)) {
|
|
2535
|
+
logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Destination mirrors the public directory layout used by the server
|
|
2540
|
+
const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
|
|
2541
|
+
if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
|
|
2542
|
+
// Ensure parent directory exists for sub-paths
|
|
2543
|
+
if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
|
|
2544
|
+
shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
|
|
2545
|
+
}
|
|
2432
2546
|
}
|
|
2433
2547
|
},
|
|
2434
2548
|
};
|
|
@@ -255,9 +255,11 @@ class Auth {
|
|
|
255
255
|
* @returns {Promise<object>} A promise resolving to the newly created guest session data.
|
|
256
256
|
*/
|
|
257
257
|
async sessionOut() {
|
|
258
|
-
// 1. End User Session
|
|
258
|
+
// 1. End User Session — skip the network call when there is no active user session
|
|
259
|
+
// (avoids a wasted DELETE round-trip for guests / new visitors)
|
|
260
|
+
const hasUserSession = !!GuestService.getUserToken();
|
|
259
261
|
try {
|
|
260
|
-
const result = await UserService.delete({ id: 'logout' });
|
|
262
|
+
const result = hasUserSession ? await UserService.delete({ id: 'logout' }) : null;
|
|
261
263
|
SearchBox.RecentResults.clear();
|
|
262
264
|
this.deleteToken();
|
|
263
265
|
if (this.#refreshTimeout) {
|
|
@@ -267,7 +269,7 @@ class Auth {
|
|
|
267
269
|
this.renderGuestUi();
|
|
268
270
|
// Reset user data in the LogIn state/model
|
|
269
271
|
LogIn.Scope.user.main.model.user = {};
|
|
270
|
-
await LogOut.Trigger(result);
|
|
272
|
+
if (result) await LogOut.Trigger(result);
|
|
271
273
|
} catch (error) {
|
|
272
274
|
logger.error('Error during user logout:', error);
|
|
273
275
|
}
|
|
@@ -279,8 +281,16 @@ class Auth {
|
|
|
279
281
|
|
|
280
282
|
if (result.status === 'success' && result.data.token) {
|
|
281
283
|
this.setGuestToken(result.data.token);
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
+
// Use the POST response data directly — avoids a redundant GET /user/auth
|
|
285
|
+
// round-trip that would otherwise re-verify the token we just received.
|
|
286
|
+
await GuestService.setMeta('guest-session', {
|
|
287
|
+
role: result.data.user?.role,
|
|
288
|
+
userId: result.data.user?._id,
|
|
289
|
+
});
|
|
290
|
+
LogIn.Scope.user.main.model.user = {};
|
|
291
|
+
await LogIn.Trigger(result.data);
|
|
292
|
+
await Account.updateForm(result.data.user);
|
|
293
|
+
return result.data;
|
|
284
294
|
} else {
|
|
285
295
|
logger.error('Failed to get a new guest token.');
|
|
286
296
|
return { user: UserMock.default };
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Css, darkTheme, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
|
|
4
|
-
import { buildBadgeToolTipMenuOption, Modal, renderViewTitle } from './Modal.js';
|
|
1
|
+
import { Css, darkTheme, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
|
|
2
|
+
import { Modal, renderViewTitle } from './Modal.js';
|
|
5
3
|
import { listenQueryPathInstance, setQueryPath, closeModalRouteChangeEvent, getProxyPath } from './Router.js';
|
|
6
|
-
import {
|
|
4
|
+
import { s, sIframe } from './VanillaJs.js';
|
|
7
5
|
// https://mintlify.com/docs/quickstart
|
|
8
6
|
class Docs {
|
|
9
7
|
static async RenderModal(type) {
|
|
@@ -289,41 +287,15 @@ class Docs {
|
|
|
289
287
|
},
|
|
290
288
|
});
|
|
291
289
|
});
|
|
292
|
-
|
|
290
|
+
// Register theme events for items that have them (Docs-specific concern)
|
|
293
291
|
for (const docData of Docs.Data) {
|
|
294
292
|
if (docData.themeEvent) {
|
|
295
293
|
ThemeEvents[`doc-icon-${docData.type}`] = docData.themeEvent;
|
|
296
294
|
setTimeout(ThemeEvents[`doc-icon-${docData.type}`]);
|
|
297
295
|
}
|
|
298
|
-
let tabHref, style, labelStyle;
|
|
299
|
-
switch (docData.type) {
|
|
300
|
-
case 'repo':
|
|
301
|
-
case 'coverage-link':
|
|
302
|
-
style = renderCssAttr({ style: { height: '45px' } });
|
|
303
|
-
labelStyle = renderCssAttr({ style: { top: '8px', left: '9px' } });
|
|
304
|
-
break;
|
|
305
|
-
default:
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
tabHref = docData.url();
|
|
309
|
-
const subMenuIcon =
|
|
310
|
-
options.subMenuIcon && typeof options.subMenuIcon === 'function'
|
|
311
|
-
? options.subMenuIcon(docData.type)
|
|
312
|
-
: docData.icon;
|
|
313
|
-
docMenuRender += html`
|
|
314
|
-
${await BtnIcon.instance({
|
|
315
|
-
class: `in wfa main-btn-menu submenu-btn btn-docs btn-docs-${docData.type}`,
|
|
316
|
-
label: html`<span class="inl menu-btn-icon">${subMenuIcon}</span
|
|
317
|
-
><span class="menu-label-text menu-label-text-docs"> ${docData.text} </span>`,
|
|
318
|
-
tabHref,
|
|
319
|
-
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption(docData.text, 'right')),
|
|
320
|
-
useMenuBtn: true,
|
|
321
|
-
})}
|
|
322
|
-
`;
|
|
323
296
|
}
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
htmls('.menu-btn-container-children-docs', docMenuRender);
|
|
297
|
+
// Build submenu items and populate — submenu system is owned by Modal
|
|
298
|
+
Modal.subMenuPopulate('docs', await Modal.buildSubMenuItemsHtml('docs', Docs.Data, options));
|
|
327
299
|
{
|
|
328
300
|
const docsData = [
|
|
329
301
|
{
|
|
@@ -469,12 +469,12 @@ class FileExplorer {
|
|
|
469
469
|
async init(params) {
|
|
470
470
|
console.log('LoadFileActionsRenderer created', params);
|
|
471
471
|
// params.data._id
|
|
472
|
-
|
|
472
|
+
this.eGui = document.createElement('div');
|
|
473
473
|
const isPublic = params.data.isPublic;
|
|
474
474
|
const toggleId = `toggle-public-${params.data._id}`;
|
|
475
475
|
const hasGenericFile = !!params.data.hasGenericFile;
|
|
476
476
|
const hasMdFile = !!params.data.hasMdFile;
|
|
477
|
-
|
|
477
|
+
this.eGui.innerHTML = html`
|
|
478
478
|
<div class="fl">
|
|
479
479
|
${await BtnIcon.instance({
|
|
480
480
|
class: `in fll management-table-btn-mini btn-file-download-${params.data._id}${!hasGenericFile ? ' btn-disabled' : ''}`,
|
|
@@ -981,7 +981,7 @@ class FileExplorer {
|
|
|
981
981
|
});
|
|
982
982
|
}
|
|
983
983
|
getGui() {
|
|
984
|
-
return
|
|
984
|
+
return this.eGui;
|
|
985
985
|
}
|
|
986
986
|
refresh(params) {
|
|
987
987
|
console.log('LoadFileActionsRenderer refreshed', params);
|
|
@@ -994,8 +994,8 @@ class FileExplorer {
|
|
|
994
994
|
console.log('LoadFolderActionsRenderer created', params);
|
|
995
995
|
// params.data._id
|
|
996
996
|
const id = params.data.locationId;
|
|
997
|
-
|
|
998
|
-
|
|
997
|
+
this.eGui = document.createElement('div');
|
|
998
|
+
this.eGui.innerHTML = html`
|
|
999
999
|
<div class="fl">
|
|
1000
1000
|
${await BtnIcon.instance({
|
|
1001
1001
|
class: `in fll management-table-btn-mini btn-folder-delete-${id}`,
|
|
@@ -1058,7 +1058,7 @@ class FileExplorer {
|
|
|
1058
1058
|
});
|
|
1059
1059
|
}
|
|
1060
1060
|
getGui() {
|
|
1061
|
-
return
|
|
1061
|
+
return this.eGui;
|
|
1062
1062
|
}
|
|
1063
1063
|
refresh(params) {
|
|
1064
1064
|
console.log('LoadFolderActionsRenderer refreshed', params);
|
|
@@ -1789,6 +1789,12 @@ class Modal {
|
|
|
1789
1789
|
if (options.onCollapseMenu) options.onCollapseMenu();
|
|
1790
1790
|
s(`.sub-menu-title-container-${'modal-menu'}`).classList.add('hide');
|
|
1791
1791
|
s(`.nav-path-container-${'modal-menu'}`).classList.add('hide');
|
|
1792
|
+
// Shrink any already-open submenu containers to icon-only width
|
|
1793
|
+
Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
|
|
1794
|
+
const container = s(`.menu-btn-container-children-${subMenuId}`);
|
|
1795
|
+
if (container && container.style.height && container.style.height !== '0px')
|
|
1796
|
+
container.style.width = `${collapseSlideMenuWidth}px`;
|
|
1797
|
+
});
|
|
1792
1798
|
Object.keys(this.Data[idModal].onCollapseMenuListener).map((keyListener) =>
|
|
1793
1799
|
this.Data[idModal].onCollapseMenuListener[keyListener](),
|
|
1794
1800
|
);
|
|
@@ -1806,6 +1812,12 @@ class Modal {
|
|
|
1806
1812
|
if (options.onExtendMenu) options.onExtendMenu();
|
|
1807
1813
|
s(`.sub-menu-title-container-${'modal-menu'}`).classList.remove('hide');
|
|
1808
1814
|
s(`.nav-path-container-${'modal-menu'}`).classList.remove('hide');
|
|
1815
|
+
// Expand any already-open submenu containers back to full width
|
|
1816
|
+
Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
|
|
1817
|
+
const container = s(`.menu-btn-container-children-${subMenuId}`);
|
|
1818
|
+
if (container && container.style.height && container.style.height !== '0px')
|
|
1819
|
+
container.style.width = `${originSlideMenuWidth}px`;
|
|
1820
|
+
});
|
|
1809
1821
|
Object.keys(this.Data[idModal].onExtendMenuListener).map((keyListener) =>
|
|
1810
1822
|
this.Data[idModal].onExtendMenuListener[keyListener](),
|
|
1811
1823
|
);
|
|
@@ -2464,6 +2476,52 @@ class Modal {
|
|
|
2464
2476
|
}
|
|
2465
2477
|
};
|
|
2466
2478
|
|
|
2479
|
+
/**
|
|
2480
|
+
* Builds submenu item HTML using the canonical BtnIcon pattern.
|
|
2481
|
+
* Centralises the item-rendering contract so individual submenu owners (e.g. Docs)
|
|
2482
|
+
* only need to supply data — not layout concerns.
|
|
2483
|
+
* @param {string} subMenuId - Submenu identifier (e.g. 'docs')
|
|
2484
|
+
* @param {Array<{type:string, icon:string, text:string, url:function|string}>} items
|
|
2485
|
+
* @param {Object} [options]
|
|
2486
|
+
* @param {function} [options.subMenuIcon] - Optional icon override per item type
|
|
2487
|
+
* @returns {Promise<string>} Rendered HTML string
|
|
2488
|
+
*/
|
|
2489
|
+
static buildSubMenuItemsHtml = async (subMenuId, items = [], options = {}) => {
|
|
2490
|
+
let result = '';
|
|
2491
|
+
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
|
|
2492
|
+
const _tooltipSide = options.tooltipSide || (_menuMode === 'slide-menu-right' ? 'right' : 'left');
|
|
2493
|
+
for (const item of items) {
|
|
2494
|
+
const tabHref = typeof item.url === 'function' ? item.url() : item.url || '';
|
|
2495
|
+
const icon =
|
|
2496
|
+
options.subMenuIcon && typeof options.subMenuIcon === 'function' ? options.subMenuIcon(item.type) : item.icon;
|
|
2497
|
+
result += html`${await BtnIcon.instance({
|
|
2498
|
+
class: `in wfa main-btn-menu submenu-btn btn-${subMenuId} btn-${subMenuId}-${item.type}`,
|
|
2499
|
+
label: html`<span class="inl menu-btn-icon">${icon}</span
|
|
2500
|
+
><span class="menu-label-text menu-label-text-${subMenuId}"> ${item.text} </span>`,
|
|
2501
|
+
tabHref,
|
|
2502
|
+
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption(item.text, _tooltipSide)),
|
|
2503
|
+
useMenuBtn: true,
|
|
2504
|
+
})} `;
|
|
2505
|
+
}
|
|
2506
|
+
return result;
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* Injects pre-built submenu item HTML into the submenu container and syncs
|
|
2511
|
+
* the collapse state so labels are hidden when the menu is icon-only.
|
|
2512
|
+
* @param {string} subMenuId
|
|
2513
|
+
* @param {string} itemsHtml
|
|
2514
|
+
*/
|
|
2515
|
+
static subMenuPopulate = (subMenuId, itemsHtml) => {
|
|
2516
|
+
htmls(`.menu-btn-container-children-${subMenuId}`, itemsHtml);
|
|
2517
|
+
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
|
|
2518
|
+
const _collapseIndicatorClass =
|
|
2519
|
+
_menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
|
|
2520
|
+
if (s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide')) {
|
|
2521
|
+
sa(`.menu-label-text-${subMenuId}`).forEach((el) => el.classList.add('hide'));
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2467
2525
|
// Move modal title element into the bar's render container so it aligns with control buttons
|
|
2468
2526
|
/**
|
|
2469
2527
|
* Position a modal relative to an anchor element.
|
|
@@ -2769,9 +2827,14 @@ const subMenuRender = async (subMenuId) => {
|
|
|
2769
2827
|
setTimeout(() => {
|
|
2770
2828
|
Modal.menuTextLabelAnimation('modal-menu', subMenuId);
|
|
2771
2829
|
});
|
|
2772
|
-
// Open animation
|
|
2830
|
+
// Open animation — match the current menu width (collapsed = 50px, extended = 320px)
|
|
2831
|
+
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
|
|
2832
|
+
const _collapseIndicatorClass =
|
|
2833
|
+
_menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
|
|
2834
|
+
const _isMenuCollapsed = s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide');
|
|
2835
|
+
const _menuContainerWidth = _isMenuCollapsed ? 50 : 320;
|
|
2773
2836
|
setTimeout(top, 360);
|
|
2774
|
-
menuContainer.style.width =
|
|
2837
|
+
menuContainer.style.width = `${_menuContainerWidth}px`;
|
|
2775
2838
|
menuContainer.style.overflow = null;
|
|
2776
2839
|
menuContainer.style.height = '0px';
|
|
2777
2840
|
menuContainer.style.height = `${_hBtn * 6}px`;
|
|
@@ -37,7 +37,7 @@ class Recover {
|
|
|
37
37
|
rules: [{ type: 'isEmpty' }, { type: 'isLength', options: { min: 2, max: 20 } }],
|
|
38
38
|
show: () => false,
|
|
39
39
|
disable: function () {
|
|
40
|
-
return !
|
|
40
|
+
return !this.show();
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
'recover-email': {
|
|
@@ -46,7 +46,7 @@ class Recover {
|
|
|
46
46
|
rules: [{ type: 'isEmpty' }, { type: 'isEmail' }],
|
|
47
47
|
show: () => mode === 'recover-verify-email',
|
|
48
48
|
disable: function () {
|
|
49
|
-
return !
|
|
49
|
+
return !this.show();
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
'recover-password': {
|
|
@@ -55,7 +55,7 @@ class Recover {
|
|
|
55
55
|
rules: [{ type: 'isStrongPassword' }],
|
|
56
56
|
show: () => mode === 'change-password',
|
|
57
57
|
disable: function () {
|
|
58
|
-
return !
|
|
58
|
+
return !this.show();
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
61
|
'recover-repeat-password': {
|
|
@@ -63,7 +63,7 @@ class Recover {
|
|
|
63
63
|
rules: [{ type: 'isEmpty' }, { type: 'passwordMismatch', options: `recover-password` }],
|
|
64
64
|
show: () => mode === 'change-password',
|
|
65
65
|
disable: function () {
|
|
66
|
-
return !
|
|
66
|
+
return !this.show();
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
69
|
};
|
|
@@ -161,36 +161,33 @@ class PwaWorker {
|
|
|
161
161
|
|
|
162
162
|
this.RouterInstance = typeof router?.instance === 'function' ? router.instance() : router();
|
|
163
163
|
if (this.RouterInstance?.Routes) registerRoutes(this.RouterInstance.Routes);
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// Defer SW registration entirely out of the render critical path.
|
|
165
|
+
// Firefox IPC for SW registration is notably slower than Chromium;
|
|
166
|
+
// scheduling after 'load' means the first meaningful paint is not blocked.
|
|
167
|
+
this.registerServiceWorkerDeferred();
|
|
166
168
|
|
|
167
169
|
// ── declarative bootstrap path ──────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
await this.runComponentInit(session.account);
|
|
188
|
-
}
|
|
189
|
-
await this.runComponentInit(Keyboard);
|
|
190
|
-
} else {
|
|
191
|
-
// ── legacy raw render callback (backward-compat) ─────────────────────
|
|
192
|
-
await render();
|
|
170
|
+
// shared core inits
|
|
171
|
+
if (themes) await Css.loadThemes(themes);
|
|
172
|
+
await this.runComponentInit(TranslateCore);
|
|
173
|
+
await this.runComponentInit(translate);
|
|
174
|
+
await this.runComponentInit(Responsive);
|
|
175
|
+
// app shell render
|
|
176
|
+
if (render && typeof render.instance === 'function') {
|
|
177
|
+
const htmlMainBody = typeof template === 'function' ? template : undefined;
|
|
178
|
+
await this.runComponentInit(render, htmlMainBody ? { htmlMainBody } : undefined);
|
|
179
|
+
}
|
|
180
|
+
// socket init
|
|
181
|
+
const channels = appStore ? appStore.Data : (session && session.socket && session.socket.Data) || undefined;
|
|
182
|
+
await this.runComponentInit(SocketIo, { channels, path: socketPath });
|
|
183
|
+
if (session) {
|
|
184
|
+
await this.runComponentInit(session.socket);
|
|
185
|
+
await this.runComponentInit(session.login);
|
|
186
|
+
await this.runComponentInit(session.logout || session.signout);
|
|
187
|
+
await this.runComponentInit(session.signup);
|
|
188
|
+
await this.runComponentInit(session.account);
|
|
193
189
|
}
|
|
190
|
+
await this.runComponentInit(Keyboard);
|
|
194
191
|
// ────────────────────────────────────────────────────────────────────────
|
|
195
192
|
await LoadRouter(this.RouterInstance);
|
|
196
193
|
LoadingAnimation.removeSplashScreen();
|
|
@@ -316,6 +313,30 @@ class PwaWorker {
|
|
|
316
313
|
}
|
|
317
314
|
}
|
|
318
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Schedules SW registration to run after the page 'load' event (or immediately
|
|
318
|
+
* if the page is already loaded). This keeps SW work off the render critical path,
|
|
319
|
+
* which is the primary cause of Firefox being slower than Chromium on first load.
|
|
320
|
+
* @memberof PwaWorker
|
|
321
|
+
* @returns {void}
|
|
322
|
+
*/
|
|
323
|
+
registerServiceWorkerDeferred() {
|
|
324
|
+
if (!('serviceWorker' in navigator)) {
|
|
325
|
+
logger.warn('Service Worker Disabled in browser');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const register = () => {
|
|
329
|
+
this.status().then((isInstall) => {
|
|
330
|
+
if (!isInstall) this.install();
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
if (document.readyState === 'complete') {
|
|
334
|
+
register();
|
|
335
|
+
} else {
|
|
336
|
+
window.addEventListener('load', register, { once: true });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
319
340
|
/**
|
|
320
341
|
* Checks the current status of all service worker registrations and sets the
|
|
321
342
|
* `updateServiceWorker` function reference if an active worker is found.
|