@stacksjs/ts-cloud 0.7.62 → 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 (76) hide show
  1. package/dist/bin/cli.js +1556 -1284
  2. package/dist/{chunk-vx2yva1a.js → chunk-0z7ry8fm.js} +897 -93
  3. package/dist/{chunk-zvk0f89t.js → chunk-28vh1h91.js} +1 -1
  4. package/dist/{chunk-ff77qy0c.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/releases.d.ts +11 -0
  20. package/dist/drivers/shared/ufw.d.ts +4 -2
  21. package/dist/index.js +4 -4
  22. package/dist/ui/access-denied.html +2 -2
  23. package/dist/ui/account/automation.html +3 -3
  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 +3 -3
  42. package/dist/ui/security.html +2 -2
  43. package/dist/ui/server/actions.html +4 -4
  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 +3 -3
  47. package/dist/ui/server/deployments.html +4 -4
  48. package/dist/ui/server/diagnostics.html +2 -2
  49. package/dist/ui/server/firewall.html +4 -4
  50. package/dist/ui/server/fleet.html +4 -4
  51. package/dist/ui/server/logs.html +4 -4
  52. package/dist/ui/server/metrics.html +203 -3
  53. package/dist/ui/server/security.html +2 -2
  54. package/dist/ui/server/services.html +2 -2
  55. package/dist/ui/server/sites.html +102 -6
  56. package/dist/ui/server/ssh-keys.html +4 -4
  57. package/dist/ui/server/team.html +4 -4
  58. package/dist/ui/server/terminal.html +2 -2
  59. package/dist/ui/serverless/alarms.html +3 -3
  60. package/dist/ui/serverless/assets.html +2 -2
  61. package/dist/ui/serverless/cost.html +2 -2
  62. package/dist/ui/serverless/data.html +4 -4
  63. package/dist/ui/serverless/deployments.html +2 -2
  64. package/dist/ui/serverless/firewall.html +2 -2
  65. package/dist/ui/serverless/functions.html +4 -4
  66. package/dist/ui/serverless/logs.html +3 -3
  67. package/dist/ui/serverless/metrics.html +2 -2
  68. package/dist/ui/serverless/queues.html +4 -4
  69. package/dist/ui/serverless/secrets.html +4 -4
  70. package/dist/ui/serverless/traces.html +4 -4
  71. package/dist/ui/serverless.html +4 -4
  72. package/dist/ui-src/pages/index.stx +12 -10
  73. package/dist/ui-src/pages/operations/observability.stx +6 -4
  74. package/dist/ui-src/pages/server/metrics.stx +262 -106
  75. package/dist/ui-src/pages/server/sites.stx +98 -6
  76. package/package.json +3 -3
@@ -19,6 +19,7 @@ function tone(s) { return s === 'live' ? 'ok' : (s === 'failed' ? 'bad' : 'warn'
19
19
  </script>
20
20
  <script client>
21
21
  import { useOpConfirm } from '../../functions/useOpConfirm.ts'
22
+ import { usePolling } from '../../functions/usePolling.ts'
22
23
 
23
24
  // Reactive site management - add, per-site deploy/edit/remove - via signals +
24
25
  // directives. Mutation responses replace the site signal without reloading, so
@@ -28,6 +29,60 @@ const addDeploy = state('server'); const addPort = state(''); const addBuild = s
28
29
  const addMsg = state(''); const addMsgKind = state('')
29
30
  const addBusy = state(false)
30
31
  const siteRows = state(initialSites)
32
+ const monitorSeries = state([])
33
+ const monitoredAt = state('')
34
+ function monitorParams() {
35
+ const to = new Date(), from = new Date(to.getTime() - 24 * 60 * 60 * 1000)
36
+ return new URLSearchParams({
37
+ from: from.toISOString(),
38
+ to: to.toISOString(),
39
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
40
+ names: 'site.health.up,site.response.time,site.http.status,site.tls.days_remaining',
41
+ aggregation: 'avg',
42
+ bucketMs: String(15 * 60 * 1000),
43
+ })
44
+ }
45
+ const { pollingState, pollingError, lastUpdatedAt, refresh } = usePolling(async () => {
46
+ const [dashboardResponse, telemetryResponse] = await Promise.all([
47
+ fetch('/api/dashboard-data'),
48
+ fetch('/api/telemetry/series?' + monitorParams()),
49
+ ])
50
+ const dashboard = await dashboardResponse.json()
51
+ const telemetry = await telemetryResponse.json()
52
+ if (!dashboardResponse.ok) throw new Error(dashboard.error || 'Site health refresh failed.')
53
+ if (!telemetryResponse.ok || telemetry.ok === false) throw new Error(telemetry.error || 'Site history refresh failed.')
54
+ if (Array.isArray(dashboard.sitesDetail)) siteRows.set(dashboard.sitesDetail)
55
+ monitorSeries.set(telemetry.series || [])
56
+ monitoredAt.set(dashboard.siteHealth?.[0]?.checkedAt || '')
57
+ }, 30000)
58
+ function monitorSource(site) {
59
+ const project = site.project || 'stacks'
60
+ const name = String(site.name || '').startsWith(project + ':') ? String(site.name).slice(project.length + 1) : site.name
61
+ return `site:${project}:${name}`
62
+ }
63
+ function metricSeries(site, name) { return monitorSeries().find(item => item.source === monitorSource(site) && item.name === name) }
64
+ function metricPoints(site, name) { return (metricSeries(site, name)?.points || []).filter(point => !point.gap && point.value != null) }
65
+ function latestMetric(site, name) { const values = metricPoints(site, name); return values.length ? Number(values.at(-1).value) : null }
66
+ function uptime(site) {
67
+ const values = metricPoints(site, 'site.health.up')
68
+ return values.length ? values.reduce((sum, point) => sum + Number(point.value || 0), 0) / values.length * 100 : null
69
+ }
70
+ function historyPath(site) {
71
+ const values = metricPoints(site, 'site.response.time')
72
+ if (!values.length) return ''
73
+ const max = Math.max(1, ...values.map(point => Number(point.value)))
74
+ return values.map((point, index) => {
75
+ const x = values.length === 1 ? 50 : index / (values.length - 1) * 100
76
+ const y = 25 - Math.max(0, Math.min(1, Number(point.value) / max)) * 22
77
+ return `${index ? 'L' : 'M'} ${x.toFixed(2)} ${y.toFixed(2)}`
78
+ }).join(' ')
79
+ }
80
+ function fmtMs(value) { return value == null ? 'collecting' : `${Math.round(value)} ms` }
81
+ function fmtUptime(value) { return value == null ? 'collecting' : `${value.toFixed(value >= 99 ? 2 : 1)}%` }
82
+ function tlsTone(days) { return days == null ? 'warn' : days < 7 ? 'bad' : days < 30 ? 'warn' : 'ok' }
83
+ const liveSites = derived(() => siteRows().filter(site => site.status === 'live').length)
84
+ const failedSites = derived(() => siteRows().filter(site => site.status === 'failed').length)
85
+ const attachedSites = derived(() => siteRows().filter(site => site.managedBy === 'attached').length)
31
86
  async function addSite() {
32
87
  if (addBusy()) return
33
88
  addBusy.set(true); addMsg.set('Adding site...'); addMsgKind.set('')
@@ -97,10 +152,17 @@ async function saveEdit(name) {
97
152
  @if (isMember)
98
153
  <div><h1>Sites</h1><p>The sites you have been given access to.</p></div>
99
154
  @else
100
- <div><h1>Sites</h1><p>Server routes served from the box, each deployed as an atomic release.</p></div>
101
- <div class="cmds header-actions"><span class="accent cmd">cloud site:add &lt;name&gt;</span></div>
155
+ <div><h1>Sites</h1><p>Every route discovered on this shared server, including attached projects.</p></div>
156
+ <div class="cmds header-actions"><span class="pill" :class="pollingState() === 'live' ? 'ok' : 'warn'">{{ pollingState() }}</span><small>{{ lastUpdatedAt()?.toLocaleTimeString() || 'connecting' }}</small><span class="accent cmd">cloud site:add &lt;name&gt;</span></div>
102
157
  @endif
103
158
  </header>
159
+ <p class="bad notice" role="status" @show="!!pollingError()">{{ pollingError() }}</p>
160
+ <div class="site-summary">
161
+ <div class="panel"><span>Routes monitored</span><strong>{{ siteRows().length }}</strong><small>{{ monitoredAt() ? 'checked ' + new Date(monitoredAt()).toLocaleTimeString() : 'checking now' }}</small></div>
162
+ <div class="panel"><span>Healthy</span><strong class="ok-text">{{ liveSites() }}</strong><small>HTTP probe below 500</small></div>
163
+ <div class="panel"><span>Failed</span><strong :class="failedSites() ? 'bad-text' : ''">{{ failedSites() }}</strong><small>Timeouts and 5xx responses</small></div>
164
+ <div class="panel"><span>Attached projects</span><strong>{{ attachedSites() }}</strong><small>Discovered from rpx registry</small></div>
165
+ </div>
104
166
  @if (!isMember)
105
167
  <div class="section">
106
168
  <h2>Add site</h2>
@@ -127,7 +189,18 @@ async function saveEdit(name) {
127
189
  @endif
128
190
  <template :for="site in siteRows()">
129
191
  <div class="section">
130
- <h2>{{ site.name }} <span class="more"><span class="pill" :class="tone(site.status)">{{ site.status }}</span></span></h2>
192
+ <h2>{{ site.name }} <span class="more"><span class="tag" @show="!!site.project">{{ site.project }}</span><span class="pill" :class="tone(site.status)">{{ site.status }}</span></span></h2>
193
+ <div class="panel site-health">
194
+ <div><span>HTTP</span><strong>{{ site.httpStatus || 'checking' }}</strong><small>{{ site.checkedAt ? new Date(site.checkedAt).toLocaleTimeString() : 'probe pending' }}</small></div>
195
+ <div><span>Response time</span><strong>{{ fmtMs(site.responseMs ?? latestMetric(site, 'site.response.time')) }}</strong><small>Latest endpoint probe</small></div>
196
+ <div><span>TLS expires</span><strong>{{ site.tlsDaysRemaining != null ? site.tlsDaysRemaining + ' days' : 'checking' }}</strong><small><span class="pill" :class="tlsTone(site.tlsDaysRemaining)">{{ site.tlsDaysRemaining == null ? 'unknown' : site.tlsDaysRemaining < 30 ? 'renew soon' : 'valid' }}</span></small></div>
197
+ <div><span>24h availability</span><strong>{{ fmtUptime(uptime(site)) }}</strong><small>{{ metricPoints(site, 'site.health.up').length }} samples</small></div>
198
+ <div class="latency-chart">
199
+ <span>24h response history</span>
200
+ <svg viewBox="0 0 100 28" preserveAspectRatio="none" role="img" :aria-label="site.name + ' response time history'"><line x1="0" x2="100" y1="25" y2="25"></line><path :d="historyPath(site)"></path></svg>
201
+ <small>{{ fmtMs(latestMetric(site, 'site.response.time')) }} latest</small>
202
+ </div>
203
+ </div>
131
204
  <div class="cols">
132
205
  <div class="panel">
133
206
  <div class="kv"><span>route</span> <b class="mono"><a :href="site.href" @show="!!site.href">{{ site.route }}</a><span @show="!site.href">{{ site.route }}</span></b></div>
@@ -139,6 +212,9 @@ async function saveEdit(name) {
139
212
  <div class="kv"><span>web root</span> <b class="mono" style="font-size:11px">{{ site.root }}</b></div>
140
213
  <div class="kv"><span>source</span> <b class="mono">{{ site.branch }}</b></div>
141
214
  <div class="kv"><span>deploy</span> <b>{{ site.deploy }}</b></div>
215
+ <div class="kv"><span>ownership</span> <b>{{ site.managedBy === 'attached' ? 'attached project' : 'server owner' }}</b></div>
216
+ <div class="kv" @show="!!site.release"><span>release</span> <b class="mono">{{ site.release }}</b></div>
217
+ <div class="kv" @show="!!site.deployedAt"><span>deployed</span> <b>{{ new Date(site.deployedAt).toLocaleString() }}</b></div>
142
218
  <div class="kv" @show="site.envKeys && site.envKeys.length"><span>env vars</span> <b class="mono">{{ (site.envKeys || []).join(', ') }}</b></div>
143
219
  <div class="kv" @show="site.aliases && site.aliases.length"><span>aliases</span> <b class="mono">{{ (site.aliases || []).join(', ') }}</b></div>
144
220
  <div class="kv" @show="site.redirects && Object.keys(site.redirects).length"><span>redirects</span> <b class="mono">{{ Object.keys(site.redirects || {}).length }}</b></div>
@@ -147,12 +223,13 @@ async function saveEdit(name) {
147
223
  </div>
148
224
  <div class="row-actions" style="margin-top:12px;">
149
225
  <a class="btn ghost sm" :href="'/operations/workloads?service=' + encodeURIComponent(site.name)">Backing workloads</a>
150
- @if (canDeploy)<button class="btn sm" type="button" @click="askDeploy(site.name)">Deploy</button>@endif
151
- @if (canEdit)<button class="btn ghost sm" type="button" @click="toggleEdit(site.name)">Edit</button>@endif
226
+ @if (canDeploy)<button class="btn sm" type="button" @show="site.managedBy !== 'attached'" @click="askDeploy(site.name)">Deploy</button>@endif
227
+ @if (canEdit)<button class="btn ghost sm" type="button" @show="site.managedBy !== 'attached'" @click="toggleEdit(site.name)">Edit</button>@endif
152
228
  @if (!isMember)
153
- <button class="btn danger sm" type="button" :aria-label="'Remove site ' + site.name" @click="askRemove(site.name)">Remove</button>
229
+ <button class="btn danger sm" type="button" @show="site.managedBy !== 'attached'" :aria-label="'Remove site ' + site.name" @click="askRemove(site.name)">Remove</button>
154
230
  @endif
155
231
  </div>
232
+ <p class="note" @show="site.managedBy === 'attached'">This route is managed by the {{ site.project }} project. Deploy and configuration actions belong in that project.</p>
156
233
  @if (!canDeploy && !canEdit)<p class="note">This membership is read-only for {{ site.name }}. Ask an organization owner for deployment or configuration access.</p>@endif
157
234
  <form class="form-grid" style="margin-top:14px;" @show="editing() === site.name" @submit.prevent="saveEdit(site.name)">
158
235
  <div class="field-block">
@@ -185,5 +262,20 @@ async function saveEdit(name) {
185
262
  </template>
186
263
  @include('../partials/op-confirm')
187
264
  </div>
265
+ <style>
266
+ .site-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:24px}
267
+ .site-summary .panel{display:grid;gap:5px;padding:14px 16px}
268
+ .site-summary span,.site-health>div>span{color:var(--txt3);font-size:11px;text-transform:uppercase;letter-spacing:.07em;font-weight:700}
269
+ .site-summary strong{font-size:24px}.site-summary small{color:var(--txt3);font-size:11px}
270
+ .ok-text{color:var(--ok)}.bad-text{color:var(--bad)}
271
+ .site-health{display:grid;grid-template-columns:repeat(4,minmax(0,1fr)) minmax(220px,1.6fr);gap:0;padding:0;margin-bottom:14px;overflow:hidden}
272
+ .site-health>div{display:grid;gap:5px;padding:15px 16px;border-right:1px solid var(--panel-br)}
273
+ .site-health>div:last-child{border-right:0}
274
+ .site-health strong{font-size:18px}.site-health small{color:var(--txt3);font-size:11px}
275
+ .site-health .pill{font-size:10px;padding:2px 7px}
276
+ .latency-chart svg{width:100%;height:36px}.latency-chart line{stroke:rgba(255,255,255,.08);vector-effect:non-scaling-stroke}.latency-chart path{fill:none;stroke:var(--accent);stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}
277
+ @media(max-width:1000px){.site-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.site-health{grid-template-columns:repeat(2,minmax(0,1fr))}.site-health>div{border-bottom:1px solid var(--panel-br)}.latency-chart{grid-column:1/-1}}
278
+ @media(max-width:600px){.site-summary,.site-health{grid-template-columns:1fr}.latency-chart{grid-column:auto}.site-health>div{border-right:0}}
279
+ </style>
188
280
  </body>
189
281
  </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/ts-cloud",
3
3
  "type": "module",
4
- "version": "0.7.62",
4
+ "version": "0.7.64",
5
5
  "description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
7
7
  "license": "MIT",
@@ -90,8 +90,8 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "@stacksjs/ts-xml": "^0.1.0",
93
- "@ts-cloud/aws-types": "0.7.62",
94
- "@ts-cloud/core": "0.7.62"
93
+ "@ts-cloud/aws-types": "0.7.64",
94
+ "@ts-cloud/core": "0.7.64"
95
95
  },
96
96
  "devDependencies": {
97
97
  "better-dx": "^0.2.16",