@chisquare-tech/origine-ngx-apps-services 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -78,6 +78,10 @@ class ServicesApi {
78
78
  deleteBackup(project, id, name) {
79
79
  return this.api.delete(this.i(project, id, 'backup/' + enc(name)));
80
80
  }
81
+ /** A link to a RabbitMQ instance's management UI, for the caller only (read-only for Developers). */
82
+ management(project, id) {
83
+ return this.api.post(this.i(project, id, 'management'), {}).pipe(map((r) => data(r)));
84
+ }
81
85
  restore(project, id, body) {
82
86
  return this.api.post(this.i(project, id, 'restore'), body).pipe(map((r) => data(r)));
83
87
  }
@@ -402,7 +406,7 @@ class ServicesListPage {
402
406
  </div>
403
407
  <div class="o-g2">
404
408
  <a class="o-card o-cb o-flex" [routerLink]="host.link('services/new')" [queryParams]="{ engine: 'nats' }" data-testid="svc-new-nats"><span class="o-sq" style="background:var(--o-ok)"></span><div class="o-col" style="gap:2px"><b>NATS with JetStream</b><span class="o-faint o-sm">Streams, consumers, KV buckets · 3-node cluster</span></div><span class="o-r o-faint o-sm">Create →</span></a>
405
- <a class="o-card o-cb o-flex" [routerLink]="host.link('services/new')" [queryParams]="{ engine: 'rabbitmq' }" data-testid="svc-new-rabbitmq"><span class="o-sq" style="background:var(--o-run)"></span><div class="o-col" style="gap:2px"><b>RabbitMQ</b><span class="o-faint o-sm">Quorum queues · inspected in the console</span></div><span class="o-r o-faint o-sm">Create →</span></a>
409
+ <a class="o-card o-cb o-flex" [routerLink]="host.link('services/new')" [queryParams]="{ engine: 'rabbitmq' }" data-testid="svc-new-rabbitmq"><span class="o-sq" style="background:var(--o-run)"></span><div class="o-col" style="gap:2px"><b>RabbitMQ</b><span class="o-faint o-sm">Quorum queues · management UI through the platform</span></div><span class="o-r o-faint o-sm">Create →</span></a>
406
410
  </div>
407
411
  <span class="o-faint o-sm">Lifecycle states: {{ lifecycle }}. Open-source engines only.</span>
408
412
  </div>
@@ -452,7 +456,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
452
456
  </div>
453
457
  <div class="o-g2">
454
458
  <a class="o-card o-cb o-flex" [routerLink]="host.link('services/new')" [queryParams]="{ engine: 'nats' }" data-testid="svc-new-nats"><span class="o-sq" style="background:var(--o-ok)"></span><div class="o-col" style="gap:2px"><b>NATS with JetStream</b><span class="o-faint o-sm">Streams, consumers, KV buckets · 3-node cluster</span></div><span class="o-r o-faint o-sm">Create →</span></a>
455
- <a class="o-card o-cb o-flex" [routerLink]="host.link('services/new')" [queryParams]="{ engine: 'rabbitmq' }" data-testid="svc-new-rabbitmq"><span class="o-sq" style="background:var(--o-run)"></span><div class="o-col" style="gap:2px"><b>RabbitMQ</b><span class="o-faint o-sm">Quorum queues · inspected in the console</span></div><span class="o-r o-faint o-sm">Create →</span></a>
459
+ <a class="o-card o-cb o-flex" [routerLink]="host.link('services/new')" [queryParams]="{ engine: 'rabbitmq' }" data-testid="svc-new-rabbitmq"><span class="o-sq" style="background:var(--o-run)"></span><div class="o-col" style="gap:2px"><b>RabbitMQ</b><span class="o-faint o-sm">Quorum queues · management UI through the platform</span></div><span class="o-r o-faint o-sm">Create →</span></a>
456
460
  </div>
457
461
  <span class="o-faint o-sm">Lifecycle states: {{ lifecycle }}. Open-source engines only.</span>
458
462
  </div>
@@ -496,7 +500,9 @@ class ServicesNewPage {
496
500
  return '3-node cluster';
497
501
  if (e?.ha === 'standby')
498
502
  return e.id === 'mysql' ? 'Standby with automatic failover (3 nodes: MOCO runs 1, 3 or 5)' : 'Standby node with automatic failover';
499
- return `${e?.name ?? 'This engine'} runs one node for now`;
503
+ if (e?.ha === 'replica')
504
+ return 'Replica with automatic failover (three Sentinels watch the primary)';
505
+ return `${e?.name ?? 'This engine'} runs one node`;
500
506
  }, ...(ngDevMode ? [{ debugName: "haLabel" }] : /* istanbul ignore next */ []));
501
507
  this.keep = signal(7, ...(ngDevMode ? [{ debugName: "keep" }] : /* istanbul ignore next */ []));
502
508
  this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
@@ -563,7 +569,7 @@ class ServicesNewPage {
563
569
  }
564
570
  pickEngine(e) {
565
571
  if (!e.available) {
566
- this.error.set(`${e.name} arrives at GA.`);
572
+ this.error.set(`${e.name} cannot be created here.`);
567
573
  return;
568
574
  }
569
575
  this.error.set('');
@@ -639,7 +645,7 @@ class ServicesNewPage {
639
645
  @for (e of engines(); track e.id) {
640
646
  <button type="button" class="o-opt" [class.o-on]="engine()?.id === e.id" [class.o-dis]="!e.available" (click)="pickEngine(e)" [attr.data-testid]="'svc-engine-' + e.id">
641
647
  <span class="o-rad" [class.o-on]="engine()?.id === e.id"></span>
642
- <div class="o-col" style="gap:3px;flex:1"><span class="o-flex"><b>{{ e.name }}</b>@if (e.tag) {<b class="o-ph-tag o-ga">{{ e.tag }}</b>}</span><span class="o-faint o-sm o-lh">{{ e.description }}</span></div>
648
+ <div class="o-col" style="gap:3px;flex:1"><span class="o-flex"><b>{{ e.name }}</b>@if (!e.available) {<span class="o-chip">Not available</span>}</span><span class="o-faint o-sm o-lh">{{ e.description }}</span></div>
643
649
  </button>
644
650
  }
645
651
  </div>
@@ -763,7 +769,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
763
769
  @for (e of engines(); track e.id) {
764
770
  <button type="button" class="o-opt" [class.o-on]="engine()?.id === e.id" [class.o-dis]="!e.available" (click)="pickEngine(e)" [attr.data-testid]="'svc-engine-' + e.id">
765
771
  <span class="o-rad" [class.o-on]="engine()?.id === e.id"></span>
766
- <div class="o-col" style="gap:3px;flex:1"><span class="o-flex"><b>{{ e.name }}</b>@if (e.tag) {<b class="o-ph-tag o-ga">{{ e.tag }}</b>}</span><span class="o-faint o-sm o-lh">{{ e.description }}</span></div>
772
+ <div class="o-col" style="gap:3px;flex:1"><span class="o-flex"><b>{{ e.name }}</b>@if (!e.available) {<span class="o-chip">Not available</span>}</span><span class="o-faint o-sm o-lh">{{ e.description }}</span></div>
767
773
  </button>
768
774
  }
769
775
  </div>
@@ -1254,7 +1260,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1254
1260
  }]
1255
1261
  }], propDecorators: { d: [{ type: i0.Input, args: [{ isSignal: true, alias: "d", required: true }] }], projectKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "projectKey", required: true }] }], dumps: [{ type: i0.Input, args: [{ isSignal: true, alias: "dumps", required: false }] }], done: [{ type: i0.Output, args: ["done"] }], failed: [{ type: i0.Output, args: ["failed"] }] } });
1256
1262
 
1257
- const COLOUR = { ERROR: 'var(--o-bad-solid)', FATAL: 'var(--o-bad-solid)', PANIC: 'var(--o-bad-solid)', WARNING: 'var(--o-run)' };
1263
+ const COLOUR = { ERROR: 'var(--o-term-del)', FATAL: 'var(--o-term-del)', PANIC: 'var(--o-term-del)', WARNING: 'var(--o-run)' };
1258
1264
  // The instance page's Logs tab (prototype "svc · logs"): the engine's own log from its pods —
1259
1265
  // all, errors, the slow log, connections — newest first, refreshed every 10 s.
1260
1266
  class ServiceLogsTab {
@@ -1461,7 +1467,7 @@ class ServiceMetricsTab {
1461
1467
  }
1462
1468
  </div>
1463
1469
  <div class="o-card">
1464
- <div class="o-ch"><b>Slow queries</b><span class="o-faint o-sm">the latest from the slow log · over 250 ms</span><b class="o-ph-tag o-ga">GA</b><span class="o-r o-faint o-sm">normalised, top by total time at GA</span></div>
1470
+ <div class="o-ch"><b>Slow queries</b><span class="o-faint o-sm">the latest from the slow log · over 250 ms</span></div>
1465
1471
  <div class="o-tbl">
1466
1472
  <div class="o-thead"><div class="o-tr"><div class="o-th">Query</div><div class="o-th o-num">Duration</div><div class="o-th o-num">When</div></div></div>
1467
1473
  <div class="o-tbody">
@@ -1505,7 +1511,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1505
1511
  }
1506
1512
  </div>
1507
1513
  <div class="o-card">
1508
- <div class="o-ch"><b>Slow queries</b><span class="o-faint o-sm">the latest from the slow log · over 250 ms</span><b class="o-ph-tag o-ga">GA</b><span class="o-r o-faint o-sm">normalised, top by total time at GA</span></div>
1514
+ <div class="o-ch"><b>Slow queries</b><span class="o-faint o-sm">the latest from the slow log · over 250 ms</span></div>
1509
1515
  <div class="o-tbl">
1510
1516
  <div class="o-thead"><div class="o-tr"><div class="o-th">Query</div><div class="o-th o-num">Duration</div><div class="o-th o-num">When</div></div></div>
1511
1517
  <div class="o-tbody">
@@ -1601,7 +1607,7 @@ class ServicePage {
1601
1607
  this.conn = computed(() => this.revealed()?.connection ?? this.d()?.connection ?? null, ...(ngDevMode ? [{ debugName: "conn" }] : /* istanbul ignore next */ []));
1602
1608
  this.snippetKeys = computed(() => {
1603
1609
  const s = this.conn()?.snippets ?? {};
1604
- return ['uri', 'psql', 'mysql', 'valkey-cli', 'nats', 'node', 'cli'].filter((k) => k in s);
1610
+ return ['uri', 'psql', 'mysql', 'valkey-cli', 'sentinel', 'nats', 'node', 'cli'].filter((k) => k in s);
1605
1611
  }, ...(ngDevMode ? [{ debugName: "snippetKeys" }] : /* istanbul ignore next */ []));
1606
1612
  this.backups = computed(() => [...(this.d()?.backups ?? [])].sort((a, b) => (b.startedAt ?? '9').localeCompare(a.startedAt ?? '9')), ...(ngDevMode ? [{ debugName: "backups" }] : /* istanbul ignore next */ []));
1607
1613
  this.attEnvs = computed(() => {
@@ -1774,7 +1780,7 @@ class ServicePage {
1774
1780
  return `${cpu} / ${ram} / ${d.diskGb} GB`;
1775
1781
  }
1776
1782
  snippetLabel(k) {
1777
- return { uri: 'URI', node: 'Node', cli: 'origine CLI', nats: 'nats' }[k] ?? k;
1783
+ return { uri: 'URI', node: 'Node', cli: 'origine CLI', nats: 'nats', sentinel: 'Sentinel' }[k] ?? k;
1778
1784
  }
1779
1785
  attState(a) {
1780
1786
  return a.state === 'live' ? 'Injected' : a.state === 'failed' ? 'Failed' : 'Redeploying…';
@@ -1903,13 +1909,14 @@ class ServicePage {
1903
1909
  return `${d.name}-restore-1`.slice(0, 40);
1904
1910
  }
1905
1911
  askRestore(d, b) {
1906
- this.dialog.set({ kind: 'restore', name: this.restoreName(d), point: '', backup: b.name });
1912
+ const vols = d.engine === 'rabbitmq' && b.volumes?.phase === 'completed';
1913
+ this.dialog.set({ kind: 'restore', name: this.restoreName(d), point: '', backup: b.name, volumes: vols, hasVolumes: vols });
1907
1914
  }
1908
1915
  askPitr(d) {
1909
1916
  this.dialog.set({ kind: 'restore', name: this.restoreName(d), point: this.pitr(), backup: '' });
1910
1917
  }
1911
1918
  restore(d, dl) {
1912
- const body = dl.point ? { pointInTime: fromLocalInput(dl.point), name: dl.name } : { backup: dl.backup, name: dl.name };
1919
+ const body = dl.point ? { pointInTime: fromLocalInput(dl.point), name: dl.name } : { backup: dl.backup, name: dl.name, volumes: !!dl.volumes };
1913
1920
  this.busy.set(true);
1914
1921
  this.error.set('');
1915
1922
  this.api.restore(this.key(), d.id, body).subscribe({
@@ -1964,6 +1971,35 @@ class ServicePage {
1964
1971
  refresh() {
1965
1972
  this.load();
1966
1973
  }
1974
+ /** Opens the instance's management UI in a new tab: a link for this person that expires. */
1975
+ openManagement(d) {
1976
+ const tab = window.open('', '_blank');
1977
+ this.busy.set(true);
1978
+ this.error.set('');
1979
+ this.api.management(this.key(), d.id).subscribe({
1980
+ next: (l) => {
1981
+ this.busy.set(false);
1982
+ if (tab) {
1983
+ tab.opener = null;
1984
+ tab.location.href = l.url;
1985
+ }
1986
+ else
1987
+ window.open(l.url, '_blank', 'noopener');
1988
+ this.notice.set(`The management UI of ${d.name} opened in a new tab${l.mode === 'read' ? ', read-only for your role' : ''}. Its link works for you for 30 minutes.`);
1989
+ },
1990
+ error: (e) => {
1991
+ this.busy.set(false);
1992
+ tab?.close();
1993
+ this.error.set(apiMessage(e, 'The management UI could not be opened.'));
1994
+ },
1995
+ });
1996
+ }
1997
+ snapshotLine(d) {
1998
+ const v = d.volumeSnapshots;
1999
+ if (v?.class)
2000
+ return `Each backup also snapshots every node's volume (${v.class}): restores can bring the messages back.`;
2001
+ return `Backups hold the definitions only: ${v?.message || 'this cell has no CSI snapshot class'}.`;
2002
+ }
1967
2003
  snapshotName(d) {
1968
2004
  return d.engine === 'nats' ? 'stream snapshot' : d.engine === 'rabbitmq' ? 'definitions export' : 'RDB snapshot';
1969
2005
  }
@@ -2034,6 +2070,7 @@ class ServicePage {
2034
2070
  <div class="o-flex"><h1 class="o-t">{{ d.name }}</h1><span [class]="chip(d.state)" data-testid="svc-state">{{ d.state }}</span></div>
2035
2071
  </div>
2036
2072
  <div class="o-acts">
2073
+ @if (d.management && !d.deletedAt) {<button type="button" class="o-btn" (click)="openManagement(d)" [disabled]="busy() || d.state !== 'Running' && d.state !== 'Updating' && d.state !== 'Degraded'" [title]="d.management === 'read' ? 'Read-only for your role' : 'As the instance administrator'" data-testid="svc-management">Open management UI@if (d.management === 'read') {<span class="o-faint o-xs"> · read-only</span>}</button>}
2037
2074
  @if (d.canManage && !d.deletedAt) {<button type="button" class="o-btn" (click)="askRotate(d)" [disabled]="busy() || !d.users?.length" data-testid="svc-rotate">Rotate credentials</button>}
2038
2075
  @if (d.canManage && !d.deletedAt) {<button type="button" class="o-btn" (click)="setTab('settings')">Resize</button>}
2039
2076
  <a class="o-btn" [routerLink]="host.link('dashboards')">Dashboard</a>
@@ -2178,9 +2215,6 @@ class ServicePage {
2178
2215
  <div class="o-cf">Per-app users: attach with “Own user” and each app gets its own least-privilege login, removed when it is detached</div>
2179
2216
  </div>
2180
2217
  </div>
2181
- @if (d.engine === 'postgres') {
2182
- <div class="o-card o-cb o-flex o-wrap"><div class="o-col" style="gap:2px"><b>Extensions</b><span class="o-hint">From the allow-list: pg_stat_statements, pgcrypto, uuid-ossp, citext, pg_trgm, postgis</span></div><div class="o-flex o-r"><b class="o-ph-tag o-ga">GA</b><button type="button" class="o-btn o-sm" disabled>+ Enable</button></div></div>
2183
- }
2184
2218
  }
2185
2219
  @case ('backups') {
2186
2220
  <div class="o-split o-w340">
@@ -2189,12 +2223,12 @@ class ServicePage {
2189
2223
  @for (b of backups(); track b.name) {
2190
2224
  <div class="o-row" [attr.data-testid]="'svc-backup-' + b.name">
2191
2225
  <span [class]="'o-dot ' + backupDot(b)"></span><span style="width:150px">{{ when(b.startedAt) || 'Pending' }}</span><span class="o-faint o-sm" style="width:80px">{{ backupNote(b) }}</span>
2192
- <span class="o-chip">{{ kind(b) }}</span><span class="o-faint o-sm">{{ keepLine(b, d.retentionDays) }}</span>
2226
+ <span class="o-chip">{{ kind(b) }}</span>@if (b.volumes) {<span [class]="'o-chip ' + (b.volumes.phase === 'completed' ? 'o-grn' : b.volumes.phase === 'failed' ? 'o-red' : 'o-blu')" [title]="b.volumes.error || b.volumes.snapshots + ' volume snapshot(s)'" data-testid="svc-backup-volumes">{{ b.volumes.phase === 'completed' ? '+ queue data' : b.volumes.phase === 'failed' ? 'queue data failed' : 'snapshotting' }}</span>}<span class="o-faint o-sm">{{ keepLine(b, d.retentionDays) }}</span>
2193
2227
  @if (b.phase === 'completed' && d.canManage) {<button type="button" class="o-lnk o-r" (click)="askRestore(d, b)">Restore</button>}
2194
2228
  @if (b.kind === 'on-demand' && d.canManage && b.phase !== 'running') {<button type="button" class="o-lnk o-faint" [class.o-r]="b.phase !== 'completed'" (click)="deleteBackup(d, b)" [attr.aria-label]="'Delete ' + b.name" [disabled]="busy()">×</button>}
2195
2229
  </div>
2196
2230
  } @empty {<div class="o-empty">No backups yet. The first is taken once {{ d.name }} is running.</div>}
2197
- <div class="o-cf">Stored encrypted in a separate failure domain. Cross-region copies<b class="o-ph-tag o-later">Later</b></div>
2231
+ <div class="o-cf">Stored encrypted in a separate failure domain.</div>
2198
2232
  </div>
2199
2233
  <div class="o-col" style="gap:14px">
2200
2234
  @if (d.pitr) {
@@ -2205,7 +2239,8 @@ class ServicePage {
2205
2239
  <button type="button" class="o-btn o-dark" (click)="askPitr(d)" [disabled]="!d.canManage || !pitr() || !d.window?.from" data-testid="svc-pitr-go">Restore…</button>
2206
2240
  </div>
2207
2241
  } @else {
2208
- <div class="o-card o-cb o-col" style="gap:6px"><b>Restore a snapshot</b><span class="o-hint">{{ d.engineName }} restores from a daily {{ snapshotName(d) }}: pick one on the left. It always creates a new instance.</span></div>
2242
+ <div class="o-card o-cb o-col" style="gap:6px"><b>Restore a snapshot</b><span class="o-hint">{{ d.engineName }} restores from a daily {{ snapshotName(d) }}: pick one on the left. It always creates a new instance.</span>
2243
+ @if (d.engine === 'rabbitmq') {<span class="o-hint" data-testid="svc-snapshot-support"><span [class]="'o-chip ' + (d.volumeSnapshots?.class ? 'o-grn' : '')">{{ d.volumeSnapshots?.class ? 'Queue data snapshots' : 'Definitions only' }}</span> {{ snapshotLine(d) }}</span>}</div>
2209
2244
  }
2210
2245
  <div class="o-card o-cb o-form">
2211
2246
  <b>Schedule</b>
@@ -2242,7 +2277,6 @@ class ServicePage {
2242
2277
  <label class="o-flex o-sm" style="gap:6px">Disk <input class="o-in o-sm o-mono" style="width:80px" type="number" [min]="d.diskGb" [max]="catalog()?.maxDiskGb ?? 2000" [disabled]="!d.canManage" [ngModel]="newDisk()" (ngModelChange)="newDisk.set(+$event)" data-testid="svc-disk" /> GB</label>
2243
2278
  <button type="button" class="o-btn o-dark" (click)="resize(d)" [disabled]="busy() || !d.canManage || (newPlan() === d.plan && newDisk() === d.diskGb)" data-testid="svc-resize">Resize</button>
2244
2279
  </div>
2245
- <div class="o-cf"><button type="button" class="o-tog" disabled aria-label="Grow storage automatically (GA)"></button>Grow storage automatically at 85% full, up to 500 GB<b class="o-ph-tag o-ga">GA</b></div>
2246
2280
  </div>
2247
2281
  <div class="o-card" data-testid="svc-ha">
2248
2282
  <div class="o-ch"><b>High availability</b><span class="o-r">{{ d.readyNodes }} of {{ d.nodes }} node{{ d.nodes === 1 ? '' : 's' }} ready</span></div>
@@ -2260,9 +2294,18 @@ class ServicePage {
2260
2294
  <div class="o-row"><span style="flex:1">3-node cluster<span class="o-faint o-sm"> · {{ d.engine === 'nats' ? 'streams replicated on three servers' : 'quorum queues on three nodes' }}</span></span><button type="button" class="o-tog" [class.o-on]="d.standby" [disabled]="!d.canManage || busy()" (click)="topology(d, !d.standby, 0)" aria-label="3-node cluster" data-testid="svc-standby"></button></div>
2261
2295
  <div class="o-cf">Each node is billed at the plan's price · {{ nodesPrice(d) }}/mo now</div>
2262
2296
  }
2297
+ @case ('replica') {
2298
+ <div class="o-row"><span style="flex:1">Replica with automatic failover<span class="o-faint o-sm"> · three Sentinels promote it if the primary fails</span></span><button type="button" class="o-tog" [class.o-on]="d.standby" [disabled]="!d.canManage || busy()" (click)="topology(d, !d.standby, 0)" aria-label="Replica with failover" data-testid="svc-standby"></button></div>
2299
+ <div class="o-row"><span style="flex:1">Second replica@if (d.sentinel; as sn) {<span class="o-faint o-sm o-mono"> · Sentinels {{ sn.hosts.length }} × :{{ sn.port }}, master {{ sn.master }}</span>}</span><span class="o-faint o-sm" data-testid="svc-replicas">{{ d.replicas }}</span>
2300
+ @if (d.canManage) {
2301
+ <button type="button" class="o-btn o-xs" (click)="topology(d, d.standby, d.replicas - 1)" [disabled]="busy() || d.replicas === 0">−</button>
2302
+ <button type="button" class="o-btn o-xs" (click)="topology(d, true, d.replicas + 1)" [disabled]="busy() || d.replicas + 1 > (engine()?.maxReplicas ?? 0)" data-testid="svc-replica-add">+ Replica</button>
2303
+ }</div>
2304
+ <div class="o-cf">Each node is billed at the plan's price · {{ nodesPrice(d) }}/mo now · apps get REDIS_SENTINEL_URL beside REDIS_URL</div>
2305
+ }
2263
2306
  @default {
2264
- <div class="o-row"><span style="flex:1">Replica with failover</span><b class="o-ph-tag o-later">Later</b></div>
2265
- <div class="o-cf">{{ d.engineName }} runs one node for now; daily snapshots keep its data.</div>
2307
+ <div class="o-row"><span style="flex:1">One node</span></div>
2308
+ <div class="o-cf">{{ d.engineName }} runs one node; daily snapshots keep its data.</div>
2266
2309
  }
2267
2310
  }
2268
2311
  </div>
@@ -2317,6 +2360,7 @@ class ServicePage {
2317
2360
  }
2318
2361
  @case ('restore') {
2319
2362
  <span>{{ dl.point ? 'Restores ' + d.name + ' as it was at ' + whenLocal(dl.point) : 'Restores backup ' + dl.backup }} into a new instance with the same users and passwords. {{ d.name }} keeps running untouched; move its apps over afterwards if you want.</span>
2363
+ @if (dl.hasVolumes) {<label class="o-flex o-sm"><input type="checkbox" [(ngModel)]="dl.volumes" data-testid="svc-restore-volumes" /> With the queue data (the backup's volume snapshots): messages, not only the definitions</label>}
2320
2364
  <label class="o-fld"><span class="o-sm">New instance</span><input class="o-in o-mono" [(ngModel)]="dl.name" data-testid="svc-restore-name" autocomplete="off" /></label>
2321
2365
  }
2322
2366
  @case ('rotate') {
@@ -2357,6 +2401,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2357
2401
  <div class="o-flex"><h1 class="o-t">{{ d.name }}</h1><span [class]="chip(d.state)" data-testid="svc-state">{{ d.state }}</span></div>
2358
2402
  </div>
2359
2403
  <div class="o-acts">
2404
+ @if (d.management && !d.deletedAt) {<button type="button" class="o-btn" (click)="openManagement(d)" [disabled]="busy() || d.state !== 'Running' && d.state !== 'Updating' && d.state !== 'Degraded'" [title]="d.management === 'read' ? 'Read-only for your role' : 'As the instance administrator'" data-testid="svc-management">Open management UI@if (d.management === 'read') {<span class="o-faint o-xs"> · read-only</span>}</button>}
2360
2405
  @if (d.canManage && !d.deletedAt) {<button type="button" class="o-btn" (click)="askRotate(d)" [disabled]="busy() || !d.users?.length" data-testid="svc-rotate">Rotate credentials</button>}
2361
2406
  @if (d.canManage && !d.deletedAt) {<button type="button" class="o-btn" (click)="setTab('settings')">Resize</button>}
2362
2407
  <a class="o-btn" [routerLink]="host.link('dashboards')">Dashboard</a>
@@ -2501,9 +2546,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2501
2546
  <div class="o-cf">Per-app users: attach with “Own user” and each app gets its own least-privilege login, removed when it is detached</div>
2502
2547
  </div>
2503
2548
  </div>
2504
- @if (d.engine === 'postgres') {
2505
- <div class="o-card o-cb o-flex o-wrap"><div class="o-col" style="gap:2px"><b>Extensions</b><span class="o-hint">From the allow-list: pg_stat_statements, pgcrypto, uuid-ossp, citext, pg_trgm, postgis</span></div><div class="o-flex o-r"><b class="o-ph-tag o-ga">GA</b><button type="button" class="o-btn o-sm" disabled>+ Enable</button></div></div>
2506
- }
2507
2549
  }
2508
2550
  @case ('backups') {
2509
2551
  <div class="o-split o-w340">
@@ -2512,12 +2554,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2512
2554
  @for (b of backups(); track b.name) {
2513
2555
  <div class="o-row" [attr.data-testid]="'svc-backup-' + b.name">
2514
2556
  <span [class]="'o-dot ' + backupDot(b)"></span><span style="width:150px">{{ when(b.startedAt) || 'Pending' }}</span><span class="o-faint o-sm" style="width:80px">{{ backupNote(b) }}</span>
2515
- <span class="o-chip">{{ kind(b) }}</span><span class="o-faint o-sm">{{ keepLine(b, d.retentionDays) }}</span>
2557
+ <span class="o-chip">{{ kind(b) }}</span>@if (b.volumes) {<span [class]="'o-chip ' + (b.volumes.phase === 'completed' ? 'o-grn' : b.volumes.phase === 'failed' ? 'o-red' : 'o-blu')" [title]="b.volumes.error || b.volumes.snapshots + ' volume snapshot(s)'" data-testid="svc-backup-volumes">{{ b.volumes.phase === 'completed' ? '+ queue data' : b.volumes.phase === 'failed' ? 'queue data failed' : 'snapshotting' }}</span>}<span class="o-faint o-sm">{{ keepLine(b, d.retentionDays) }}</span>
2516
2558
  @if (b.phase === 'completed' && d.canManage) {<button type="button" class="o-lnk o-r" (click)="askRestore(d, b)">Restore</button>}
2517
2559
  @if (b.kind === 'on-demand' && d.canManage && b.phase !== 'running') {<button type="button" class="o-lnk o-faint" [class.o-r]="b.phase !== 'completed'" (click)="deleteBackup(d, b)" [attr.aria-label]="'Delete ' + b.name" [disabled]="busy()">×</button>}
2518
2560
  </div>
2519
2561
  } @empty {<div class="o-empty">No backups yet. The first is taken once {{ d.name }} is running.</div>}
2520
- <div class="o-cf">Stored encrypted in a separate failure domain. Cross-region copies<b class="o-ph-tag o-later">Later</b></div>
2562
+ <div class="o-cf">Stored encrypted in a separate failure domain.</div>
2521
2563
  </div>
2522
2564
  <div class="o-col" style="gap:14px">
2523
2565
  @if (d.pitr) {
@@ -2528,7 +2570,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2528
2570
  <button type="button" class="o-btn o-dark" (click)="askPitr(d)" [disabled]="!d.canManage || !pitr() || !d.window?.from" data-testid="svc-pitr-go">Restore…</button>
2529
2571
  </div>
2530
2572
  } @else {
2531
- <div class="o-card o-cb o-col" style="gap:6px"><b>Restore a snapshot</b><span class="o-hint">{{ d.engineName }} restores from a daily {{ snapshotName(d) }}: pick one on the left. It always creates a new instance.</span></div>
2573
+ <div class="o-card o-cb o-col" style="gap:6px"><b>Restore a snapshot</b><span class="o-hint">{{ d.engineName }} restores from a daily {{ snapshotName(d) }}: pick one on the left. It always creates a new instance.</span>
2574
+ @if (d.engine === 'rabbitmq') {<span class="o-hint" data-testid="svc-snapshot-support"><span [class]="'o-chip ' + (d.volumeSnapshots?.class ? 'o-grn' : '')">{{ d.volumeSnapshots?.class ? 'Queue data snapshots' : 'Definitions only' }}</span> {{ snapshotLine(d) }}</span>}</div>
2532
2575
  }
2533
2576
  <div class="o-card o-cb o-form">
2534
2577
  <b>Schedule</b>
@@ -2565,7 +2608,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2565
2608
  <label class="o-flex o-sm" style="gap:6px">Disk <input class="o-in o-sm o-mono" style="width:80px" type="number" [min]="d.diskGb" [max]="catalog()?.maxDiskGb ?? 2000" [disabled]="!d.canManage" [ngModel]="newDisk()" (ngModelChange)="newDisk.set(+$event)" data-testid="svc-disk" /> GB</label>
2566
2609
  <button type="button" class="o-btn o-dark" (click)="resize(d)" [disabled]="busy() || !d.canManage || (newPlan() === d.plan && newDisk() === d.diskGb)" data-testid="svc-resize">Resize</button>
2567
2610
  </div>
2568
- <div class="o-cf"><button type="button" class="o-tog" disabled aria-label="Grow storage automatically (GA)"></button>Grow storage automatically at 85% full, up to 500 GB<b class="o-ph-tag o-ga">GA</b></div>
2569
2611
  </div>
2570
2612
  <div class="o-card" data-testid="svc-ha">
2571
2613
  <div class="o-ch"><b>High availability</b><span class="o-r">{{ d.readyNodes }} of {{ d.nodes }} node{{ d.nodes === 1 ? '' : 's' }} ready</span></div>
@@ -2583,9 +2625,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2583
2625
  <div class="o-row"><span style="flex:1">3-node cluster<span class="o-faint o-sm"> · {{ d.engine === 'nats' ? 'streams replicated on three servers' : 'quorum queues on three nodes' }}</span></span><button type="button" class="o-tog" [class.o-on]="d.standby" [disabled]="!d.canManage || busy()" (click)="topology(d, !d.standby, 0)" aria-label="3-node cluster" data-testid="svc-standby"></button></div>
2584
2626
  <div class="o-cf">Each node is billed at the plan's price · {{ nodesPrice(d) }}/mo now</div>
2585
2627
  }
2628
+ @case ('replica') {
2629
+ <div class="o-row"><span style="flex:1">Replica with automatic failover<span class="o-faint o-sm"> · three Sentinels promote it if the primary fails</span></span><button type="button" class="o-tog" [class.o-on]="d.standby" [disabled]="!d.canManage || busy()" (click)="topology(d, !d.standby, 0)" aria-label="Replica with failover" data-testid="svc-standby"></button></div>
2630
+ <div class="o-row"><span style="flex:1">Second replica@if (d.sentinel; as sn) {<span class="o-faint o-sm o-mono"> · Sentinels {{ sn.hosts.length }} × :{{ sn.port }}, master {{ sn.master }}</span>}</span><span class="o-faint o-sm" data-testid="svc-replicas">{{ d.replicas }}</span>
2631
+ @if (d.canManage) {
2632
+ <button type="button" class="o-btn o-xs" (click)="topology(d, d.standby, d.replicas - 1)" [disabled]="busy() || d.replicas === 0">−</button>
2633
+ <button type="button" class="o-btn o-xs" (click)="topology(d, true, d.replicas + 1)" [disabled]="busy() || d.replicas + 1 > (engine()?.maxReplicas ?? 0)" data-testid="svc-replica-add">+ Replica</button>
2634
+ }</div>
2635
+ <div class="o-cf">Each node is billed at the plan's price · {{ nodesPrice(d) }}/mo now · apps get REDIS_SENTINEL_URL beside REDIS_URL</div>
2636
+ }
2586
2637
  @default {
2587
- <div class="o-row"><span style="flex:1">Replica with failover</span><b class="o-ph-tag o-later">Later</b></div>
2588
- <div class="o-cf">{{ d.engineName }} runs one node for now; daily snapshots keep its data.</div>
2638
+ <div class="o-row"><span style="flex:1">One node</span></div>
2639
+ <div class="o-cf">{{ d.engineName }} runs one node; daily snapshots keep its data.</div>
2589
2640
  }
2590
2641
  }
2591
2642
  </div>
@@ -2640,6 +2691,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2640
2691
  }
2641
2692
  @case ('restore') {
2642
2693
  <span>{{ dl.point ? 'Restores ' + d.name + ' as it was at ' + whenLocal(dl.point) : 'Restores backup ' + dl.backup }} into a new instance with the same users and passwords. {{ d.name }} keeps running untouched; move its apps over afterwards if you want.</span>
2694
+ @if (dl.hasVolumes) {<label class="o-flex o-sm"><input type="checkbox" [(ngModel)]="dl.volumes" data-testid="svc-restore-volumes" /> With the queue data (the backup's volume snapshots): messages, not only the definitions</label>}
2643
2695
  <label class="o-fld"><span class="o-sm">New instance</span><input class="o-in o-mono" [(ngModel)]="dl.name" data-testid="svc-restore-name" autocomplete="off" /></label>
2644
2696
  }
2645
2697
  @case ('rotate') {