@stacksjs/ts-cloud 0.7.63 → 0.7.65

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 (73) hide show
  1. package/dist/bin/cli.js +1556 -1284
  2. package/dist/{chunk-0zbqbrqg.js → chunk-0z7ry8fm.js} +889 -89
  3. package/dist/{chunk-t092y37p.js → chunk-267f6qd0.js} +525 -145
  4. package/dist/{chunk-zvk0f89t.js → chunk-28vh1h91.js} +1 -1
  5. package/dist/{chunk-tq46tnxv.js → chunk-ecsyc3p0.js} +4 -1
  6. package/dist/{chunk-rxhqny6z.js → chunk-zcvdzvmz.js} +2 -2
  7. package/dist/deploy/dashboard-data-server.d.ts +27 -1
  8. package/dist/deploy/index.js +4 -4
  9. package/dist/deploy/telemetry-collection.d.ts +2 -0
  10. package/dist/drivers/hetzner/client.d.ts +84 -2
  11. package/dist/drivers/hetzner/host-optimization.d.ts +33 -0
  12. package/dist/drivers/hetzner/monitoring.d.ts +46 -0
  13. package/dist/drivers/hetzner/resize-remote.d.ts +38 -0
  14. package/dist/drivers/hetzner/resize-state.d.ts +24 -0
  15. package/dist/drivers/hetzner/resize.d.ts +86 -0
  16. package/dist/drivers/index.d.ts +8 -0
  17. package/dist/drivers/index.js +35 -3
  18. package/dist/drivers/shared/compute-deploy.d.ts +15 -0
  19. package/dist/drivers/shared/ufw.d.ts +4 -2
  20. package/dist/index.js +4 -4
  21. package/dist/security/pre-deploy-scanner.test.d.ts +1 -0
  22. package/dist/ui/access-denied.html +2 -2
  23. package/dist/ui/account/automation.html +4 -4
  24. package/dist/ui/account/security.html +2 -2
  25. package/dist/ui/applications/compose.html +4 -4
  26. package/dist/ui/applications/new.html +2 -2
  27. package/dist/ui/data/backups.html +4 -4
  28. package/dist/ui/data/services.html +4 -4
  29. package/dist/ui/data/volumes.html +4 -4
  30. package/dist/ui/index.html +12 -7
  31. package/dist/ui/integrations.html +2 -2
  32. package/dist/ui/operations/alerts.html +4 -4
  33. package/dist/ui/operations/configuration.html +4 -4
  34. package/dist/ui/operations/jobs.html +4 -4
  35. package/dist/ui/operations/maintenance.html +3 -3
  36. package/dist/ui/operations/observability.html +13 -7
  37. package/dist/ui/operations/previews.html +4 -4
  38. package/dist/ui/operations/queue.html +4 -4
  39. package/dist/ui/operations/regions.html +4 -4
  40. package/dist/ui/operations/releases.html +4 -4
  41. package/dist/ui/operations/workloads.html +4 -4
  42. package/dist/ui/security.html +2 -2
  43. package/dist/ui/server/actions.html +3 -3
  44. package/dist/ui/server/activity.html +2 -2
  45. package/dist/ui/server/capacity.html +4 -4
  46. package/dist/ui/server/database.html +4 -4
  47. package/dist/ui/server/deployments.html +4 -4
  48. package/dist/ui/server/firewall.html +3 -3
  49. package/dist/ui/server/fleet.html +4 -4
  50. package/dist/ui/server/logs.html +4 -4
  51. package/dist/ui/server/metrics.html +203 -3
  52. package/dist/ui/server/services.html +2 -2
  53. package/dist/ui/server/sites.html +102 -6
  54. package/dist/ui/server/ssh-keys.html +4 -4
  55. package/dist/ui/server/team.html +4 -4
  56. package/dist/ui/server/terminal.html +2 -2
  57. package/dist/ui/serverless/alarms.html +4 -4
  58. package/dist/ui/serverless/assets.html +2 -2
  59. package/dist/ui/serverless/cost.html +2 -2
  60. package/dist/ui/serverless/data.html +4 -4
  61. package/dist/ui/serverless/firewall.html +2 -2
  62. package/dist/ui/serverless/functions.html +4 -4
  63. package/dist/ui/serverless/logs.html +3 -3
  64. package/dist/ui/serverless/metrics.html +2 -2
  65. package/dist/ui/serverless/queues.html +4 -4
  66. package/dist/ui/serverless/secrets.html +4 -4
  67. package/dist/ui/serverless/traces.html +4 -4
  68. package/dist/ui/serverless.html +4 -4
  69. package/dist/ui-src/pages/index.stx +12 -10
  70. package/dist/ui-src/pages/operations/observability.stx +6 -4
  71. package/dist/ui-src/pages/server/metrics.stx +262 -106
  72. package/dist/ui-src/pages/server/sites.stx +98 -6
  73. package/package.json +3 -3
@@ -9,7 +9,7 @@ import {
9
9
  step,
10
10
  success,
11
11
  warn
12
- } from "./chunk-zvk0f89t.js";
12
+ } from "./chunk-28vh1h91.js";
13
13
  import {
14
14
  AwsBackupClient,
15
15
  CloudWatchLogsClient,
@@ -45,7 +45,7 @@ import {
45
45
  resolveSiteKind,
46
46
  resolveUiSource,
47
47
  siteInstallBase
48
- } from "./chunk-0zbqbrqg.js";
48
+ } from "./chunk-0z7ry8fm.js";
49
49
  import {
50
50
  artifactKey,
51
51
  buildCloudFormationTemplate,
@@ -67,7 +67,7 @@ import {
67
67
  resolveStatePath,
68
68
  setStateDir,
69
69
  statePath
70
- } from "./chunk-tq46tnxv.js";
70
+ } from "./chunk-ecsyc3p0.js";
71
71
  import {
72
72
  SSMClient
73
73
  } from "./chunk-703nkybg.js";
@@ -8317,7 +8317,7 @@ async function deployServerlessApp(config, environment2, opts = {}) {
8317
8317
  }
8318
8318
  if (imageMode) {
8319
8319
  step("Building + pushing container image");
8320
- const { buildAndPushServerlessImage } = await import("./chunk-rxhqny6z.js");
8320
+ const { buildAndPushServerlessImage } = await import("./chunk-zcvdzvmz.js");
8321
8321
  const built = await buildAndPushServerlessImage({
8322
8322
  app,
8323
8323
  projectRoot,
@@ -36946,7 +36946,7 @@ var SECRET_PATTERNS = [
36946
36946
  },
36947
36947
  {
36948
36948
  name: "AWS Secret Access Key",
36949
- pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key|secretAccessKey)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi,
36949
+ pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key|secretAccessKey)['"]?\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi,
36950
36950
  severity: "critical",
36951
36951
  description: "AWS Secret Access Key detected"
36952
36952
  },
@@ -37289,7 +37289,11 @@ class PreDeployScanner {
37289
37289
  if (this.isLikelyPlaceholder(match[0], contextLine)) {
37290
37290
  continue;
37291
37291
  }
37292
- if (this.isLowEntropy(match[1] || match[0])) {
37292
+ const candidate2 = match[1] || match[0];
37293
+ if (pattern.name === "AWS Secret Key (Generic)" && /^[a-f0-9]{40}$/i.test(candidate2)) {
37294
+ continue;
37295
+ }
37296
+ if (this.isLowEntropy(candidate2)) {
37293
37297
  continue;
37294
37298
  }
37295
37299
  findings.push({
@@ -38367,28 +38371,183 @@ var PROBED_SERVICES = [
38367
38371
  "meilisearch"
38368
38372
  ];
38369
38373
  var UNKNOWN = "-";
38370
- function metricsScript() {
38371
- return [
38374
+ function sharedBoxProbeScript(projectFilter) {
38375
+ const source = String.raw`
38376
+ import { basename, join } from 'node:path'
38377
+ import { existsSync, readdirSync, readFileSync, readlinkSync, statSync } from 'node:fs'
38378
+
38379
+ const fragmentsRoot = '/etc/rpx/sites.d'
38380
+ const certsRoot = '/etc/rpx/certs'
38381
+ const projectFilter = ${JSON.stringify(projectFilter ?? "")}
38382
+ const configuredRoutes = []
38383
+ if (existsSync(fragmentsRoot)) {
38384
+ for (const file of readdirSync(fragmentsRoot).filter(name => name.endsWith('.json')).sort()) {
38385
+ try {
38386
+ const fragment = JSON.parse(readFileSync(join(fragmentsRoot, file), 'utf8'))
38387
+ const project = String(fragment.slug || basename(file, '.json'))
38388
+ if (projectFilter && project !== projectFilter) continue
38389
+ for (const route of Array.isArray(fragment.proxies) ? fragment.proxies : []) {
38390
+ if (!route || !route.to) continue
38391
+ const path = route.path || '/'
38392
+ const staticRoot = typeof route.static === 'string' ? route.static : route.static?.dir
38393
+ const kind = route.redirect ? 'redirect' : staticRoot ? 'static' : 'app'
38394
+ const install = staticRoot || ''
38395
+ const installName = install.match(/^\/var\/www\/(.+?)\/current(?:\/|$)/)?.[1]
38396
+ const siteName = installName?.startsWith(project + '-') ? installName.slice(project.length + 1) : String(route.id || route.to)
38397
+ const href = 'https://' + route.to + (path === '/' ? '' : path)
38398
+ configuredRoutes.push({
38399
+ name: siteName,
38400
+ project,
38401
+ route: path === '/' ? route.to : route.to + path,
38402
+ href,
38403
+ domain: route.to,
38404
+ path,
38405
+ kind,
38406
+ runtime: kind === 'app' ? 'service' : kind,
38407
+ deploy: kind === 'app' ? 'service' : kind === 'static' ? 'server static' : 'redirect',
38408
+ tls: 'https',
38409
+ root: install || (Array.isArray(route.from) ? route.from.join(', ') : route.from || 'gateway'),
38410
+ branch: kind === 'static' ? 'build artifact' : 'main',
38411
+ install,
38412
+ })
38413
+ }
38414
+ }
38415
+ catch {}
38416
+ }
38417
+ }
38418
+
38419
+ const routes = await Promise.all(configuredRoutes.map(async route => {
38420
+ const started = performance.now()
38421
+ let httpStatus = 0
38422
+ const probe = async (href) => {
38423
+ const response = await fetch(href, {
38424
+ redirect: 'manual',
38425
+ headers: { 'user-agent': 'ts-cloud-monitor/1.0' },
38426
+ signal: AbortSignal.timeout(6000),
38427
+ })
38428
+ const status = response.status
38429
+ await response.body?.cancel()
38430
+ return status
38431
+ }
38432
+ try {
38433
+ httpStatus = await probe(route.href)
38434
+ if (httpStatus === 404 && route.kind === 'app') {
38435
+ const healthHref = route.href.replace(/\/$/, '') + '/health'
38436
+ httpStatus = await probe(healthHref)
38437
+ }
38438
+ }
38439
+ catch {}
38440
+ const responseMs = Math.max(0, Math.round(performance.now() - started))
38441
+ let tlsDaysRemaining
38442
+ const cert = join(certsRoot, route.domain + '.crt')
38443
+ if (existsSync(cert)) {
38444
+ const result = Bun.spawnSync(['openssl', 'x509', '-in', cert, '-noout', '-enddate'])
38445
+ const expiry = result.success ? result.stdout.toString().trim().replace(/^notAfter=/, '') : ''
38446
+ const expiresAt = Date.parse(expiry)
38447
+ if (Number.isFinite(expiresAt)) tlsDaysRemaining = Math.floor((expiresAt - Date.now()) / 86400000)
38448
+ }
38449
+ let release
38450
+ let deployedAt
38451
+ if (route.install) {
38452
+ try {
38453
+ release = basename(readlinkSync(route.install))
38454
+ deployedAt = statSync(route.install).mtime.toISOString()
38455
+ }
38456
+ catch {}
38457
+ }
38458
+ const status = httpStatus === 0 || httpStatus >= 500 ? 'failed' : httpStatus === 404 ? 'degraded' : 'live'
38459
+ const { install, ...site } = route
38460
+ return {
38461
+ ...site,
38462
+ status,
38463
+ httpStatus,
38464
+ responseMs,
38465
+ tlsDaysRemaining,
38466
+ checkedAt: new Date().toISOString(),
38467
+ release,
38468
+ deployedAt,
38469
+ }
38470
+ }))
38471
+ for (const route of routes) console.log('DISCOVERED_SITE=' + Buffer.from(JSON.stringify(route)).toString('base64'))
38472
+
38473
+ const serviceOutput = Bun.spawnSync([
38474
+ 'systemctl', 'list-units', '--type=service', '--state=running', '--no-legend', '--plain',
38475
+ ]).stdout.toString()
38476
+ for (const line of serviceOutput.split('\n')) {
38477
+ const unit = line.trim().split(/\s+/)[0]
38478
+ if (!unit) continue
38479
+ const properties = Bun.spawnSync([
38480
+ 'systemctl',
38481
+ 'show',
38482
+ unit,
38483
+ '--property=WorkingDirectory,ExecStart,MemoryCurrent,UnitFileState,ActiveEnterTimestamp',
38484
+ ]).stdout.toString()
38485
+ const values = Object.fromEntries(properties.split('\n').filter(Boolean).map(value => {
38486
+ const separator = value.indexOf('=')
38487
+ return separator < 0 ? [value, ''] : [value.slice(0, separator), value.slice(separator + 1)]
38488
+ }))
38489
+ if (!/^\/var\/www\/|^\/opt\/ts-cloud\//.test(values.WorkingDirectory || '') && !/\/var\/www\/|\/opt\/ts-cloud\//.test(values.ExecStart || '')) continue
38490
+ console.log([
38491
+ 'SVC',
38492
+ unit,
38493
+ 'active',
38494
+ values.MemoryCurrent || '0',
38495
+ values.UnitFileState || '-',
38496
+ values.ActiveEnterTimestamp || '-',
38497
+ ].join('='))
38498
+ }
38499
+ `;
38500
+ return `echo '${Buffer.from(source).toString("base64")}' | base64 -d | bun -`;
38501
+ }
38502
+ function metricsScript(options = {}) {
38503
+ const commands = [
38372
38504
  "set +e",
38373
38505
  'echo "CPUS=$(nproc 2>/dev/null || echo 1)"',
38506
+ `read TS_TOTAL_1 TS_IDLE_1 <<EOF
38507
+ $(awk '/^cpu /{idle=$5+$6; total=0; for(i=2;i<=NF;i++) total+=$i; print total, idle; exit}' /proc/stat 2>/dev/null)
38508
+ EOF`,
38509
+ "sleep 0.2",
38510
+ `read TS_TOTAL_2 TS_IDLE_2 <<EOF
38511
+ $(awk '/^cpu /{idle=$5+$6; total=0; for(i=2;i<=NF;i++) total+=$i; print total, idle; exit}' /proc/stat 2>/dev/null)
38512
+ EOF`,
38513
+ `echo "CPUUSED=$(awk -v t1="$TS_TOTAL_1" -v i1="$TS_IDLE_1" -v t2="$TS_TOTAL_2" -v i2="$TS_IDLE_2" 'BEGIN { dt=t2-t1; di=i2-i1; if (dt>0) printf "%.1f", 100*(dt-di)/dt; else print 0 }')"`,
38374
38514
  `echo "LOAD=$(cut -d' ' -f1 /proc/loadavg 2>/dev/null || echo 0)"`,
38515
+ `echo "LOAD5=$(cut -d' ' -f2 /proc/loadavg 2>/dev/null || echo 0)"`,
38516
+ `echo "LOAD15=$(cut -d' ' -f3 /proc/loadavg 2>/dev/null || echo 0)"`,
38375
38517
  `echo "MEMTOTAL=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}')"`,
38376
38518
  `echo "MEMUSED=$(free -m 2>/dev/null | awk '/^Mem:/{print $3}')"`,
38519
+ `echo "MEMAVAILABLE=$(free -m 2>/dev/null | awk '/^Mem:/{print $7}')"`,
38520
+ `echo "MEMCACHE=$(free -m 2>/dev/null | awk '/^Mem:/{print $6}')"`,
38521
+ `echo "SWAPTOTAL=$(free -m 2>/dev/null | awk '/^Swap:/{print $2}')"`,
38522
+ `echo "SWAPUSED=$(free -m 2>/dev/null | awk '/^Swap:/{print $3}')"`,
38377
38523
  `echo "DISKPCT=$(df -P / 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')"`,
38378
38524
  `echo "DISKUSEDG=$(df -BG / 2>/dev/null | awk 'NR==2{gsub("G","",$3);print $3}')"`,
38379
38525
  `echo "DISKTOTG=$(df -BG / 2>/dev/null | awk 'NR==2{gsub("G","",$2);print $2}')"`,
38526
+ `echo "INODEPCT=$(df -Pi / 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')"`,
38527
+ `echo "NETRX=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{sum+=$3} END{print sum+0}' /proc/net/dev 2>/dev/null)"`,
38528
+ `echo "NETTX=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{sum+=$11} END{print sum+0}' /proc/net/dev 2>/dev/null)"`,
38529
+ `echo "PROCESSES=$(ps -e --no-headers 2>/dev/null | awk 'END{print NR+0}')"`,
38380
38530
  `echo "UPTIME=$(uptime -p 2>/dev/null | sed 's/^up //' || echo unknown)"`,
38381
- 'echo "OS=$(. /etc/os-release 2>/dev/null; echo "$PRETTY_NAME")"',
38382
- `for s in ${PROBED_SERVICES.join(" ")}; do st=$(systemctl is-active "$s" 2>/dev/null); if [ -n "$st" ] && [ "$st" != "inactive" ] && [ "$st" != "unknown" ]; then mem=$(systemctl show "$s" -p MemoryCurrent --value 2>/dev/null); en=$(systemctl is-enabled "$s" 2>/dev/null); since=$(systemctl show "$s" -p ActiveEnterTimestamp --value 2>/dev/null); echo "SVC=$s=$st=$mem=$en=$since"; fi; done`,
38383
- "true"
38531
+ `echo "UPTIME_SECONDS=$(cut -d' ' -f1 /proc/uptime 2>/dev/null || echo 0)"`,
38532
+ 'echo "OS=$(. /etc/os-release 2>/dev/null; echo "$PRETTY_NAME")"'
38384
38533
  ];
38534
+ if (options.includeServices !== false)
38535
+ commands.push(`for s in ${PROBED_SERVICES.join(" ")}; do st=$(systemctl is-active "$s" 2>/dev/null); if [ -n "$st" ] && [ "$st" != "inactive" ] && [ "$st" != "unknown" ]; then mem=$(systemctl show "$s" -p MemoryCurrent --value 2>/dev/null); en=$(systemctl is-enabled "$s" 2>/dev/null); since=$(systemctl show "$s" -p ActiveEnterTimestamp --value 2>/dev/null); echo "SVC=$s=$st=$mem=$en=$since"; fi; done`);
38536
+ commands.push(sharedBoxProbeScript(options.projectFilter), "true");
38537
+ return commands;
38385
38538
  }
38386
38539
  function parseBlock(output) {
38387
- const kv = { services: [] };
38540
+ const kv = { services: [], discoveredSites: [] };
38388
38541
  for (const line of output.split(`
38389
38542
  `)) {
38390
38543
  const l = line.trim();
38391
- if (l.startsWith("SVC=")) {
38544
+ if (l.startsWith("DISCOVERED_SITE=")) {
38545
+ try {
38546
+ const site = JSON.parse(Buffer.from(l.slice("DISCOVERED_SITE=".length), "base64").toString("utf8"));
38547
+ if (site?.domain && site?.route)
38548
+ kv.discoveredSites.push(site);
38549
+ } catch {}
38550
+ } else if (l.startsWith("SVC=")) {
38392
38551
  const [, name, status, memBytes, enabled, since] = /^SVC=([^=]+)=([^=]+)(?:=([^=]*)(?:=([^=]*)(?:=(.*))?)?)?$/.exec(l) ?? [];
38393
38552
  if (name) {
38394
38553
  kv.services.push({
@@ -38574,6 +38733,30 @@ function configuredSites(config6) {
38574
38733
  };
38575
38734
  });
38576
38735
  }
38736
+ function mergeDiscoveredSites(configured, discovered, ownerSlug) {
38737
+ const routes = new Map(configured.map((site) => [String(site.route), { ...site, project: ownerSlug, managedBy: "owner" }]));
38738
+ for (const site of discovered) {
38739
+ const current = routes.get(site.route);
38740
+ const name = current?.name ?? `${site.project}:${site.name}`;
38741
+ routes.set(site.route, {
38742
+ ...current,
38743
+ ...site,
38744
+ name,
38745
+ kind: current?.kind ?? site.kind,
38746
+ type: current?.type ?? site.kind,
38747
+ runtime: current?.runtime ?? site.runtime,
38748
+ deploy: current?.deploy ?? site.deploy,
38749
+ root: current?.root ?? site.root,
38750
+ branch: current?.branch ?? site.branch,
38751
+ managedBy: site.project === ownerSlug ? "owner" : "attached"
38752
+ });
38753
+ }
38754
+ return [...routes.values()].sort((a, b) => {
38755
+ if (a.managedBy !== b.managedBy)
38756
+ return a.managedBy === "owner" ? -1 : 1;
38757
+ return String(a.route).localeCompare(String(b.route));
38758
+ });
38759
+ }
38577
38760
  function shellSingleQuote(value2) {
38578
38761
  return `'${value2.replaceAll("'", "'\\''")}'`;
38579
38762
  }
@@ -38944,12 +39127,24 @@ function resolveConfigOnlyServerDashboardData(config6, environment2) {
38944
39127
  },
38945
39128
  systemMetrics: {
38946
39129
  load: 0,
39130
+ load5: 0,
39131
+ load15: 0,
39132
+ cpuUsedPct: 0,
38947
39133
  cpus: Math.max(1, Number(config6.infrastructure?.compute?.instances ?? 1)),
38948
39134
  memUsedMb: 0,
38949
39135
  memTotalMb: 0,
39136
+ memAvailableMb: 0,
39137
+ memCacheMb: 0,
39138
+ swapUsedMb: 0,
39139
+ swapTotalMb: 0,
38950
39140
  diskUsedPct: 0,
38951
39141
  diskUsedGb: 0,
38952
- diskTotalGb: diskSize
39142
+ diskTotalGb: diskSize,
39143
+ inodeUsedPct: 0,
39144
+ networkRxBytes: 0,
39145
+ networkTxBytes: 0,
39146
+ processes: 0,
39147
+ uptimeSeconds: 0
38953
39148
  },
38954
39149
  metricsUnavailable: true,
38955
39150
  services,
@@ -38980,6 +39175,7 @@ function resolveConfigOnlyServerDashboardData(config6, environment2) {
38980
39175
  envKeys: Object.keys(site.env ?? site.environment ?? {})
38981
39176
  };
38982
39177
  }),
39178
+ siteHealth: [],
38983
39179
  sshKeys: describeSshKeys(config6.infrastructure?.compute?.sshKeys ?? []),
38984
39180
  _serverReachable: false,
38985
39181
  _metricsStatus: "unavailable"
@@ -38988,7 +39184,7 @@ function resolveConfigOnlyServerDashboardData(config6, environment2) {
38988
39184
  out.activity = activityFeed(out);
38989
39185
  return out;
38990
39186
  }
38991
- async function resolveServerDashboardData(config6, environment2) {
39187
+ async function resolveServerDashboardData(config6, environment2, options = {}) {
38992
39188
  if (!config6.infrastructure?.compute)
38993
39189
  return null;
38994
39190
  let driver = null;
@@ -39007,7 +39203,10 @@ async function resolveServerDashboardData(config6, environment2) {
39007
39203
  if (targets.length) {
39008
39204
  const result2 = await driver.runRemoteDeploy({
39009
39205
  targets: [targets[0]],
39010
- commands: metricsScript(),
39206
+ commands: metricsScript({
39207
+ includeServices: !options.telemetryOnly,
39208
+ projectFilter: config6.cloud?.attachTo ? config6.project.slug : undefined
39209
+ }),
39011
39210
  comment: `ts-cloud dashboard:build ${config6.project.slug}`,
39012
39211
  tags: { Project: config6.project.slug, Environment: environment2, Role: "app" }
39013
39212
  });
@@ -39029,19 +39228,32 @@ async function resolveServerDashboardData(config6, environment2) {
39029
39228
  out._metricsStatus = "live";
39030
39229
  out.systemMetrics = {
39031
39230
  load: num(parsed.LOAD),
39231
+ load5: num(parsed.LOAD5),
39232
+ load15: num(parsed.LOAD15),
39233
+ cpuUsedPct: num(parsed.CPUUSED),
39032
39234
  cpus: num(parsed.CPUS, 1),
39033
39235
  memUsedMb: num(parsed.MEMUSED),
39034
39236
  memTotalMb: num(parsed.MEMTOTAL, 1),
39237
+ memAvailableMb: num(parsed.MEMAVAILABLE),
39238
+ memCacheMb: num(parsed.MEMCACHE),
39239
+ swapUsedMb: num(parsed.SWAPUSED),
39240
+ swapTotalMb: num(parsed.SWAPTOTAL),
39035
39241
  diskUsedPct: num(parsed.DISKPCT),
39036
39242
  diskUsedGb: num(parsed.DISKUSEDG),
39037
- diskTotalGb: num(parsed.DISKTOTG, 1)
39243
+ diskTotalGb: num(parsed.DISKTOTG, 1),
39244
+ inodeUsedPct: num(parsed.INODEPCT),
39245
+ networkRxBytes: num(parsed.NETRX),
39246
+ networkTxBytes: num(parsed.NETTX),
39247
+ processes: num(parsed.PROCESSES),
39248
+ uptimeSeconds: num(parsed.UPTIME_SECONDS)
39038
39249
  };
39039
39250
  if (parsed.services.length) {
39040
- out.services = parsed.services.map((s) => ({
39251
+ const services = [...new Map(parsed.services.map((service16) => [service16.name, service16])).values()];
39252
+ out.services = services.map((s) => ({
39041
39253
  name: s.name,
39042
39254
  status: s.status === "active" ? "running" : s.status
39043
39255
  }));
39044
- out.servicesDetail = parsed.services.map((s) => ({
39256
+ out.servicesDetail = services.map((s) => ({
39045
39257
  name: s.name,
39046
39258
  status: s.status === "active" ? "running" : s.status,
39047
39259
  since: s.since && s.since !== UNKNOWN ? s.since : out.server.uptime,
@@ -39049,64 +39261,100 @@ async function resolveServerDashboardData(config6, environment2) {
39049
39261
  auto: s.enabled ? !["disabled", "static", "masked"].includes(s.enabled) : true
39050
39262
  }));
39051
39263
  }
39052
- }
39053
- if (driver && targets.length) {
39054
- try {
39055
- const siteNames = Object.keys(config6.sites ?? {});
39056
- const historyResult = await driver.runRemoteDeploy({
39057
- targets: [targets[0]],
39058
- commands: deployHistoryScript(siteNames, config6.project.slug),
39059
- comment: `ts-cloud dashboard:deploy-history ${config6.project.slug}`,
39060
- tags: { Project: config6.project.slug, Environment: environment2, Role: "app" }
39061
- });
39062
- const historyOutput = historyResult.perInstance?.[0]?.output ?? "";
39063
- const records = parseDeployHistory(historyOutput, config6.sites);
39064
- out.serverDeployments = records.slice(0, 5);
39065
- out.serverDeploymentsDetail = records.slice(0, 50);
39066
- out.deploymentsEmptyReason = records.length ? undefined : "No deployment history was found on the server yet. Deploy again to populate this timeline.";
39067
- } catch {
39068
- out.deploymentsEmptyReason = "Deployment history could not be read from the server.";
39264
+ if (parsed.discoveredSites.length) {
39265
+ out.sitesDetail = mergeDiscoveredSites(out.sitesDetail, parsed.discoveredSites, config6.project.slug);
39266
+ out.sites = out.sitesDetail.map((site) => ({
39267
+ name: site.name,
39268
+ project: site.project,
39269
+ managedBy: site.managedBy,
39270
+ route: site.route,
39271
+ href: site.href,
39272
+ domain: site.domain,
39273
+ path: site.path,
39274
+ kind: site.kind,
39275
+ type: site.type,
39276
+ runtime: site.runtime,
39277
+ deploy: site.deploy,
39278
+ tls: site.tls,
39279
+ status: site.status,
39280
+ httpStatus: site.httpStatus,
39281
+ responseMs: site.responseMs,
39282
+ tlsDaysRemaining: site.tlsDaysRemaining,
39283
+ checkedAt: site.checkedAt
39284
+ }));
39285
+ out.siteHealth = out.sitesDetail.map((site) => ({
39286
+ name: site.name,
39287
+ project: site.project,
39288
+ route: site.route,
39289
+ status: site.status,
39290
+ httpStatus: site.httpStatus,
39291
+ responseMs: site.responseMs,
39292
+ tlsDaysRemaining: site.tlsDaysRemaining,
39293
+ checkedAt: site.checkedAt
39294
+ }));
39069
39295
  }
39296
+ }
39297
+ if (driver && targets.length && !options.telemetryOnly) {
39298
+ await Promise.all([
39299
+ (async () => {
39300
+ try {
39301
+ const siteNames = Object.keys(config6.sites ?? {});
39302
+ const historyResult = await driver.runRemoteDeploy({
39303
+ targets: [targets[0]],
39304
+ commands: deployHistoryScript(siteNames, config6.project.slug),
39305
+ comment: `ts-cloud dashboard:deploy-history ${config6.project.slug}`,
39306
+ tags: { Project: config6.project.slug, Environment: environment2, Role: "app" }
39307
+ });
39308
+ const historyOutput = historyResult.perInstance?.[0]?.output ?? "";
39309
+ const records = parseDeployHistory(historyOutput, config6.sites);
39310
+ out.serverDeployments = records.slice(0, 5);
39311
+ out.serverDeploymentsDetail = records.slice(0, 50);
39312
+ out.deploymentsEmptyReason = records.length ? undefined : "No deployment history was found on the server yet. Deploy again to populate this timeline.";
39313
+ } catch {
39314
+ out.deploymentsEmptyReason = "Deployment history could not be read from the server.";
39315
+ }
39316
+ })(),
39317
+ (async () => {
39318
+ try {
39319
+ const logsResult = await driver.runRemoteDeploy({
39320
+ targets: [targets[0]],
39321
+ commands: serverLogsScript(config6),
39322
+ comment: `ts-cloud dashboard:server-logs ${config6.project.slug}`,
39323
+ tags: { Project: config6.project.slug, Environment: environment2, Role: "app" }
39324
+ });
39325
+ const logsOutput = logsResult.perInstance?.[0]?.output ?? "";
39326
+ const records = parseServerLogs(logsOutput);
39327
+ out.serverLogs = records;
39328
+ out.serverLogsEmptyReason = records.length ? undefined : "No recent journal entries were found for the managed server units.";
39329
+ } catch {
39330
+ out.serverLogsEmptyReason = "Server logs could not be read from the box.";
39331
+ }
39332
+ })(),
39333
+ (async () => {
39334
+ try {
39335
+ const securityResult = await driver.runRemoteDeploy({
39336
+ targets: [targets[0]],
39337
+ commands: securityScript(config6),
39338
+ comment: `ts-cloud dashboard:security ${config6.project.slug}`,
39339
+ tags: { Project: config6.project.slug, Environment: environment2, Role: "app" }
39340
+ });
39341
+ const securityOutput = securityResult.perInstance?.[0]?.output ?? "";
39342
+ const liveSecurity = parseServerSecurity(securityOutput);
39343
+ const declaredSecurity = configuredSecurity(config6);
39344
+ if (!liveSecurity.ports.length)
39345
+ liveSecurity.ports = declaredSecurity.ports;
39346
+ if (!liveSecurity.tlsCertificates.length)
39347
+ liveSecurity.tlsCertificates = declaredSecurity.tlsCertificates;
39348
+ out.security = liveSecurity;
39349
+ out.securityEmptyReason = undefined;
39350
+ } catch {
39351
+ out.securityEmptyReason = "Server security checks could not be read from the box.";
39352
+ }
39353
+ })()
39354
+ ]);
39070
39355
  } else if (driver && instanceCount === 0) {
39071
39356
  out.deploymentsEmptyReason = "No app server target was found for this environment.";
39072
39357
  }
39073
- if (driver && targets.length) {
39074
- try {
39075
- const logsResult = await driver.runRemoteDeploy({
39076
- targets: [targets[0]],
39077
- commands: serverLogsScript(config6),
39078
- comment: `ts-cloud dashboard:server-logs ${config6.project.slug}`,
39079
- tags: { Project: config6.project.slug, Environment: environment2, Role: "app" }
39080
- });
39081
- const logsOutput = logsResult.perInstance?.[0]?.output ?? "";
39082
- const records = parseServerLogs(logsOutput);
39083
- out.serverLogs = records;
39084
- out.serverLogsEmptyReason = records.length ? undefined : "No recent journal entries were found for the managed server units.";
39085
- } catch {
39086
- out.serverLogsEmptyReason = "Server logs could not be read from the box.";
39087
- }
39088
- }
39089
- if (driver && targets.length) {
39090
- try {
39091
- const securityResult = await driver.runRemoteDeploy({
39092
- targets: [targets[0]],
39093
- commands: securityScript(config6),
39094
- comment: `ts-cloud dashboard:security ${config6.project.slug}`,
39095
- tags: { Project: config6.project.slug, Environment: environment2, Role: "app" }
39096
- });
39097
- const securityOutput = securityResult.perInstance?.[0]?.output ?? "";
39098
- const liveSecurity = parseServerSecurity(securityOutput);
39099
- const declaredSecurity = configuredSecurity(config6);
39100
- if (!liveSecurity.ports.length)
39101
- liveSecurity.ports = declaredSecurity.ports;
39102
- if (!liveSecurity.tlsCertificates.length)
39103
- liveSecurity.tlsCertificates = declaredSecurity.tlsCertificates;
39104
- out.security = liveSecurity;
39105
- out.securityEmptyReason = undefined;
39106
- } catch {
39107
- out.securityEmptyReason = "Server security checks could not be read from the box.";
39108
- }
39109
- }
39110
39358
  const sshKeys = config6.infrastructure?.compute?.sshKeys ?? config6.infrastructure?.ssh?.keys ?? config6.infrastructure?.sshKeys;
39111
39359
  if (Array.isArray(sshKeys) && sshKeys.length) {
39112
39360
  out.sshKeys = describeSshKeys(sshKeys);
@@ -41982,7 +42230,9 @@ async function collectNow(context) {
41982
42230
  const scope2 = { projectId: context.projectId, environmentId: context.environmentId };
41983
42231
  const mode = resolveDeploymentMode(context.config);
41984
42232
  try {
41985
- const data = (mode === "serverless" ? await resolveDashboardData(context.config, context.environment) : await resolveServerDashboardData(context.config, context.environment)) ?? {};
42233
+ const data = (mode === "serverless" ? await resolveDashboardData(context.config, context.environment) : await resolveServerDashboardData(context.config, context.environment, {
42234
+ telemetryOnly: context.lightweight
42235
+ })) ?? {};
41986
42236
  if (mode === "serverless") {
41987
42237
  for (const fn of data.functionsDetail ?? []) {
41988
42238
  const resourceId = resourceBySlug(context.controlPlane, context.projectId, context.environmentId, fn.key);
@@ -42151,14 +42401,30 @@ async function collectNow(context) {
42151
42401
  if (data.metricsUnavailable || !metrics)
42152
42402
  errors.push({ source: "host", message: "The host metrics probe is unavailable." });
42153
42403
  else {
42404
+ const memoryUsedPercent = Number(metrics.memTotalMb) > 0 ? Number(metrics.memUsedMb) / Number(metrics.memTotalMb) * 100 : 0;
42405
+ const swapUsedPercent = Number(metrics.swapTotalMb) > 0 ? Number(metrics.swapUsedMb) / Number(metrics.swapTotalMb) * 100 : 0;
42154
42406
  const values = [
42155
42407
  ["host.load", Number(metrics.load), "load"],
42408
+ ["host.load.5m", Number(metrics.load5), "load"],
42409
+ ["host.load.15m", Number(metrics.load15), "load"],
42156
42410
  ["host.cpu.capacity", Number(metrics.cpus), "count"],
42411
+ ["host.cpu.used_percent", Number(metrics.cpuUsedPct), "percent"],
42157
42412
  ["host.memory.used", Number(metrics.memUsedMb) * 1024 * 1024, "bytes"],
42158
42413
  ["host.memory.total", Number(metrics.memTotalMb) * 1024 * 1024, "bytes"],
42414
+ ["host.memory.available", Number(metrics.memAvailableMb) * 1024 * 1024, "bytes"],
42415
+ ["host.memory.cache", Number(metrics.memCacheMb) * 1024 * 1024, "bytes"],
42416
+ ["host.memory.used_percent", memoryUsedPercent, "percent"],
42417
+ ["host.swap.used", Number(metrics.swapUsedMb) * 1024 * 1024, "bytes"],
42418
+ ["host.swap.total", Number(metrics.swapTotalMb) * 1024 * 1024, "bytes"],
42419
+ ["host.swap.used_percent", swapUsedPercent, "percent"],
42159
42420
  ["host.disk.used_percent", Number(metrics.diskUsedPct), "percent"],
42160
42421
  ["host.disk.used", Number(metrics.diskUsedGb) * 1024 ** 3, "bytes"],
42161
- ["host.disk.total", Number(metrics.diskTotalGb) * 1024 ** 3, "bytes"]
42422
+ ["host.disk.total", Number(metrics.diskTotalGb) * 1024 ** 3, "bytes"],
42423
+ ["host.disk.inode_used_percent", Number(metrics.inodeUsedPct), "percent"],
42424
+ ["host.network.rx", Number(metrics.networkRxBytes), "bytes"],
42425
+ ["host.network.tx", Number(metrics.networkTxBytes), "bytes"],
42426
+ ["host.processes", Number(metrics.processes), "count"],
42427
+ ["host.uptime", Number(metrics.uptimeSeconds), "seconds"]
42162
42428
  ];
42163
42429
  for (const [name, value2, unit] of values)
42164
42430
  if (Number.isFinite(value2))
@@ -42172,6 +42438,43 @@ async function collectNow(context) {
42172
42438
  value: value2,
42173
42439
  unit
42174
42440
  });
42441
+ for (const site of data.siteHealth ?? []) {
42442
+ const project = String(site.project ?? context.config.project.slug);
42443
+ const displayName = String(site.name);
42444
+ const siteName = displayName.startsWith(`${project}:`) ? displayName.slice(project.length + 1) : displayName;
42445
+ const source = `site:${project}:${siteName}`;
42446
+ const resourceId = resourceBySlug(context.controlPlane, context.projectId, context.environmentId, displayName);
42447
+ const base = {
42448
+ ...scope2,
42449
+ resourceId,
42450
+ source,
42451
+ timestamp: site.checkedAt || now.toISOString(),
42452
+ host: site.route,
42453
+ attributes: {
42454
+ project,
42455
+ site: siteName,
42456
+ route: String(site.route),
42457
+ status: String(site.status)
42458
+ }
42459
+ };
42460
+ const siteValues = [
42461
+ ["site.health.up", site.status === "live" ? 1 : 0, "boolean"],
42462
+ ["site.http.status", Number(site.httpStatus), "status"],
42463
+ ["site.response.time", Number(site.responseMs), "ms"]
42464
+ ];
42465
+ if (site.tlsDaysRemaining != null)
42466
+ siteValues.push(["site.tls.days_remaining", Number(site.tlsDaysRemaining), "days"]);
42467
+ for (const [name, value2, unit] of siteValues)
42468
+ if (Number.isFinite(value2))
42469
+ records.push({
42470
+ ...base,
42471
+ id: id(source, name, base.timestamp),
42472
+ kind: "metric",
42473
+ name,
42474
+ value: value2,
42475
+ unit
42476
+ });
42477
+ }
42175
42478
  }
42176
42479
  }
42177
42480
  } catch (error2) {
@@ -42180,73 +42483,74 @@ async function collectNow(context) {
42180
42483
  message: error2 instanceof Error ? error2.message : String(error2)
42181
42484
  });
42182
42485
  }
42183
- try {
42184
- const inventory2 = await resolveRuntimeInventory(context.config, context.environment);
42185
- const runtime3 = new RuntimeOperationService(context.config, context.environment, {
42186
- inventory: async () => inventory2
42187
- });
42188
- for (const workload of inventory2.workloads) {
42189
- const resourceId = resourceBySlug(context.controlPlane, context.projectId, context.environmentId, workload.links.service);
42190
- const base = {
42191
- ...scope2,
42192
- resourceId,
42193
- source: `runtime:${workload.provider}`,
42194
- timestamp: now.toISOString(),
42195
- workloadId: workload.id,
42196
- releaseId: workload.links.release,
42197
- attributes: { status: workload.status, provider: workload.provider, service: workload.links.service ?? "" }
42198
- };
42199
- records.push({
42200
- ...base,
42201
- id: id(workload.id, "health", now.toISOString()),
42202
- kind: "metric",
42203
- name: "health.up",
42204
- value: workload.status === "running" ? 1 : 0,
42205
- unit: "boolean"
42206
- });
42207
- records.push({
42208
- ...base,
42209
- id: id(workload.id, "restarts", now.toISOString()),
42210
- kind: "metric",
42211
- name: "saturation.restarts",
42212
- value: workload.restartCount ?? 0,
42213
- unit: "count"
42486
+ if (!context.lightweight)
42487
+ try {
42488
+ const inventory2 = await resolveRuntimeInventory(context.config, context.environment);
42489
+ const runtime3 = new RuntimeOperationService(context.config, context.environment, {
42490
+ inventory: async () => inventory2
42214
42491
  });
42215
- if (workload.resources?.memoryBytes != null)
42492
+ for (const workload of inventory2.workloads) {
42493
+ const resourceId = resourceBySlug(context.controlPlane, context.projectId, context.environmentId, workload.links.service);
42494
+ const base = {
42495
+ ...scope2,
42496
+ resourceId,
42497
+ source: `runtime:${workload.provider}`,
42498
+ timestamp: now.toISOString(),
42499
+ workloadId: workload.id,
42500
+ releaseId: workload.links.release,
42501
+ attributes: { status: workload.status, provider: workload.provider, service: workload.links.service ?? "" }
42502
+ };
42216
42503
  records.push({
42217
42504
  ...base,
42218
- id: id(workload.id, "memory", now.toISOString()),
42505
+ id: id(workload.id, "health", now.toISOString()),
42219
42506
  kind: "metric",
42220
- name: "runtime.memory.used",
42221
- value: workload.resources.memoryBytes,
42222
- unit: "bytes"
42507
+ name: "health.up",
42508
+ value: workload.status === "running" ? 1 : 0,
42509
+ unit: "boolean"
42223
42510
  });
42224
- if (!workload.capabilities.logs.supported)
42225
- continue;
42226
- try {
42227
- const result2 = await runtime3.logs(workload.id, { limit: 100, since: new Date(now.getTime() - 5 * 60000) });
42228
- for (const line of result2.lines)
42229
- records.push(...telemetryRecordsFromLog({
42230
- ...scope2,
42231
- resourceId,
42232
- source: `runtime:${workload.provider}`,
42233
- name: `${workload.links.service ?? workload.name}.log`,
42234
- timestamp: line.timestamp,
42235
- message: line.message,
42236
- workloadId: workload.id
42237
- }));
42238
- } catch (error2) {
42239
- errors.push({
42240
- source: `runtime:${workload.provider}`,
42241
- message: error2 instanceof Error ? error2.message : String(error2)
42511
+ records.push({
42512
+ ...base,
42513
+ id: id(workload.id, "restarts", now.toISOString()),
42514
+ kind: "metric",
42515
+ name: "saturation.restarts",
42516
+ value: workload.restartCount ?? 0,
42517
+ unit: "count"
42242
42518
  });
42519
+ if (workload.resources?.memoryBytes != null)
42520
+ records.push({
42521
+ ...base,
42522
+ id: id(workload.id, "memory", now.toISOString()),
42523
+ kind: "metric",
42524
+ name: "runtime.memory.used",
42525
+ value: workload.resources.memoryBytes,
42526
+ unit: "bytes"
42527
+ });
42528
+ if (!workload.capabilities.logs.supported)
42529
+ continue;
42530
+ try {
42531
+ const result2 = await runtime3.logs(workload.id, { limit: 100, since: new Date(now.getTime() - 5 * 60000) });
42532
+ for (const line of result2.lines)
42533
+ records.push(...telemetryRecordsFromLog({
42534
+ ...scope2,
42535
+ resourceId,
42536
+ source: `runtime:${workload.provider}`,
42537
+ name: `${workload.links.service ?? workload.name}.log`,
42538
+ timestamp: line.timestamp,
42539
+ message: line.message,
42540
+ workloadId: workload.id
42541
+ }));
42542
+ } catch (error2) {
42543
+ errors.push({
42544
+ source: `runtime:${workload.provider}`,
42545
+ message: error2 instanceof Error ? error2.message : String(error2)
42546
+ });
42547
+ }
42243
42548
  }
42549
+ for (const source of inventory2.sources.filter((source2) => source2.status !== "fresh"))
42550
+ errors.push({ source: source.id, message: source.message ?? `Runtime source is ${source.status}.` });
42551
+ } catch (error2) {
42552
+ errors.push({ source: "runtime", message: error2 instanceof Error ? error2.message : String(error2) });
42244
42553
  }
42245
- for (const source of inventory2.sources.filter((source2) => source2.status !== "fresh"))
42246
- errors.push({ source: source.id, message: source.message ?? `Runtime source is ${source.status}.` });
42247
- } catch (error2) {
42248
- errors.push({ source: "runtime", message: error2 instanceof Error ? error2.message : String(error2) });
42249
- }
42250
42554
  for (const event of context.controlPlane.listEvents({ projectId: context.projectId, limit: 1000 })) {
42251
42555
  const payload2 = event.payload;
42252
42556
  records.push({
@@ -42293,8 +42597,9 @@ async function collectNow(context) {
42293
42597
  }
42294
42598
  async function collectDashboardTelemetry(context) {
42295
42599
  const key2 = `${context.projectId}:${context.environmentId ?? "all"}:${context.environment}`;
42600
+ const tieredKey = `${key2}:${context.lightweight ? "lightweight" : "full"}`;
42296
42601
  const ttl = resolveDeploymentMode(context.config) === "serverless" ? 5 * 60000 : 60000;
42297
- const result2 = await collectionCache.getOrCreate(key2, ttl, !!context.force, () => collectNow(context));
42602
+ const result2 = await collectionCache.getOrCreate(tieredKey, ttl, !!context.force, () => collectNow(context));
42298
42603
  return { ...result2.value, cached: result2.cached };
42299
42604
  }
42300
42605
  function invalidateDashboardTelemetryCache(projectId) {
@@ -42634,7 +42939,23 @@ function isTrustedMutationRequest(req) {
42634
42939
  if (site === "cross-site")
42635
42940
  return false;
42636
42941
  const origin = req.headers.get("origin");
42637
- return !origin || origin === new URL(req.url).origin;
42942
+ if (!origin)
42943
+ return true;
42944
+ const requestUrl = new URL(req.url);
42945
+ if (origin === requestUrl.origin)
42946
+ return true;
42947
+ if (site !== "same-origin")
42948
+ return false;
42949
+ try {
42950
+ const originUrl = new URL(origin);
42951
+ const forwardedHost = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? requestUrl.host).split(",")[0].trim();
42952
+ const forwardedProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
42953
+ if (!/^[A-Za-z0-9.-]+(?::\d{1,5})?$/.test(forwardedHost) || originUrl.host !== forwardedHost)
42954
+ return false;
42955
+ return !forwardedProto || originUrl.protocol === `${forwardedProto}:`;
42956
+ } catch {
42957
+ return false;
42958
+ }
42638
42959
  }
42639
42960
  var ORGANIZATION_ROLES = new Set(["owner", "admin", "deployer", "operator", "viewer", "auditor"]);
42640
42961
  var AUTHORIZATION_SCOPES = new Set(["organization", "project", "environment", "resource"]);
@@ -43109,7 +43430,52 @@ async function startLocalDashboardServer(options = {}) {
43109
43430
  const configPath = resolveCloudConfigPath(cwd);
43110
43431
  const initialData = await resolveLiveDashboardData(config6, defaultEnvironment);
43111
43432
  const latestDataByEnvironment = new Map([[defaultEnvironment, initialData]]);
43433
+ const liveDataRefreshesByEnvironment = new Map;
43434
+ const refreshLatestDashboardData = (environment2) => {
43435
+ const running = liveDataRefreshesByEnvironment.get(environment2);
43436
+ if (running)
43437
+ return running;
43438
+ const refresh = resolveLiveDashboardData(config6, environment2).then((data) => {
43439
+ const previous = latestDataByEnvironment.get(environment2);
43440
+ latestDataByEnvironment.set(environment2, data);
43441
+ if (previous?._serverReachable !== data._serverReachable || previous?._metricsStatus !== data._metricsStatus || (previous?.sitesDetail?.length ?? 0) !== (data.sitesDetail?.length ?? 0))
43442
+ clearUiCache();
43443
+ return data;
43444
+ }).finally(() => liveDataRefreshesByEnvironment.delete(environment2));
43445
+ liveDataRefreshesByEnvironment.set(environment2, refresh);
43446
+ return refresh;
43447
+ };
43112
43448
  const packagedUi = resolveUiSource(cwd);
43449
+ const telemetryPreference = process.env.TS_CLOUD_DASHBOARD_TELEMETRY?.trim();
43450
+ const backgroundTelemetryEnabled = options.box && telemetryPreference !== "0" && (telemetryPreference === "1" || !config6.cloud?.attachTo);
43451
+ if (backgroundTelemetryEnabled) {
43452
+ let telemetryCollectionRunning = false;
43453
+ const collectHostHistory = async () => {
43454
+ if (telemetryCollectionRunning)
43455
+ return;
43456
+ telemetryCollectionRunning = true;
43457
+ try {
43458
+ const environmentRecord = controlPlane.environments.get(defaultEnvironment);
43459
+ await collectDashboardTelemetry({
43460
+ controlPlane: controlPlane.store,
43461
+ projectId: controlPlane.project.id,
43462
+ environmentId: environmentRecord?.id,
43463
+ config: config6,
43464
+ environment: defaultEnvironment,
43465
+ force: true,
43466
+ lightweight: true
43467
+ });
43468
+ } catch (error2) {
43469
+ if (options.verbose)
43470
+ console.warn(` ts-cloud dashboard: background telemetry collection failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
43471
+ } finally {
43472
+ telemetryCollectionRunning = false;
43473
+ }
43474
+ };
43475
+ collectHostHistory();
43476
+ const telemetryTimer = setInterval(() => void collectHostHistory(), 60000);
43477
+ telemetryTimer.unref?.();
43478
+ }
43113
43479
  const uiCache = new Map;
43114
43480
  const uiBuilds = new Map;
43115
43481
  const ownedUiRoots = new Set;
@@ -43683,6 +44049,7 @@ async function startLocalDashboardServer(options = {}) {
43683
44049
  const server = Bun.serve({
43684
44050
  hostname: host2,
43685
44051
  port,
44052
+ idleTimeout: 30,
43686
44053
  websocket: {
43687
44054
  open(ws) {
43688
44055
  if (!terminalEnabled) {
@@ -43727,8 +44094,7 @@ async function startLocalDashboardServer(options = {}) {
43727
44094
  const environment2 = resolveDashboardEnvironment(availableEnvironments, defaultEnvironment, requestedEnvironment);
43728
44095
  let latestData = latestDataByEnvironment.get(environment2);
43729
44096
  if (!latestData) {
43730
- latestData = await resolveLiveDashboardData(config6, environment2);
43731
- latestDataByEnvironment.set(environment2, latestData);
44097
+ latestData = await refreshLatestDashboardData(environment2);
43732
44098
  }
43733
44099
  try {
43734
44100
  if (!isTrustedMutationRequest(req))
@@ -47021,10 +47387,22 @@ This replacement link expires ${created.invitation.expiresAt} and can be used on
47021
47387
  environmentId: environmentRecord?.id,
47022
47388
  config: config6,
47023
47389
  environment: environment2,
47024
- force
47390
+ force,
47391
+ lightweight: !force
47025
47392
  });
47393
+ const collectionSnapshot = () => {
47394
+ const saved = controlPlane.store.getSetting(`telemetry.collection:${controlPlane.project.id}:${environmentRecord?.id ?? "all"}`) ?? {};
47395
+ return {
47396
+ generatedAt: String(saved.generatedAt ?? new Date().toISOString()),
47397
+ cached: true,
47398
+ statuses: Array.isArray(saved.statuses) ? saved.statuses : [],
47399
+ errors: Array.isArray(saved.errors) ? saved.errors : [],
47400
+ policy: saved.policy ?? loadTelemetryPolicy(controlPlane.store, controlPlane.project.id)
47401
+ };
47402
+ };
47026
47403
  if (url.pathname === "/api/telemetry/status" && req.method === "GET") {
47027
- const collection2 = await collect(false);
47404
+ collect(false);
47405
+ const collection2 = collectionSnapshot();
47028
47406
  const scopedStatuses = environmentWide ? collection2.statuses : telemetry2.status(controlPlane.project.id, environmentRecord?.id, collection2.policy.rawDays, collection2.policy.samplingRate, readableResources.map((resource2) => resource2.id));
47029
47407
  const estimatedMonthlyBytes = scopedStatuses.reduce((sum, source) => sum + source.estimatedDailyBytes * 30, 0);
47030
47408
  return json15({
@@ -47053,7 +47431,8 @@ This replacement link expires ${created.invitation.expiresAt} and can be used on
47053
47431
  return json15({ ok: true, policy: policy3 });
47054
47432
  }
47055
47433
  if (url.pathname === "/api/telemetry/query" && req.method === "GET") {
47056
- const collection2 = await collect(false);
47434
+ collect(false);
47435
+ const collection2 = collectionSnapshot();
47057
47436
  return json15({
47058
47437
  ok: true,
47059
47438
  collection: { generatedAt: collection2.generatedAt, cached: collection2.cached },
@@ -47061,7 +47440,7 @@ This replacement link expires ${created.invitation.expiresAt} and can be used on
47061
47440
  });
47062
47441
  }
47063
47442
  if (url.pathname === "/api/telemetry/tail" && req.method === "GET") {
47064
- await collect(false);
47443
+ collect(false);
47065
47444
  const encoder = new TextEncoder;
47066
47445
  const initialTo = new Date;
47067
47446
  const baseQuery = queryFrom({
@@ -47142,7 +47521,8 @@ data: ${JSON.stringify({ cursor: position, at: new Date().toISOString() })}
47142
47521
  });
47143
47522
  }
47144
47523
  if (url.pathname === "/api/telemetry/series" && req.method === "GET") {
47145
- const collection2 = await collect(false);
47524
+ collect(false);
47525
+ const collection2 = collectionSnapshot();
47146
47526
  const aggregation = ["sum", "avg", "min", "max", "count", "p50", "p90", "p95", "p99"].includes(String(urlInput.aggregation)) ? urlInput.aggregation : "avg";
47147
47527
  const query = queryFrom({ ...urlInput, kinds: "metric", limit: 5000 });
47148
47528
  const series = telemetry2.series({
@@ -48363,8 +48743,8 @@ data: ${JSON.stringify({ cursor: position, at: new Date().toISOString() })}
48363
48743
  });
48364
48744
  }
48365
48745
  if (url.pathname === "/api/dashboard-data") {
48366
- latestData = await resolveLiveDashboardData(config6, environment2);
48367
- latestDataByEnvironment.set(environment2, latestData);
48746
+ latestData = latestDataByEnvironment.get(environment2) ?? await refreshLatestDashboardData(environment2);
48747
+ refreshLatestDashboardData(environment2);
48368
48748
  return json15(scopeDashboardData(latestData, { user: scopedUser, slug: config6.project.slug }));
48369
48749
  }
48370
48750
  if (url.pathname === "/api/users" && req.method === "GET")