@stacksjs/ts-cloud 0.7.63 → 0.7.64

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