@chisquare-tech/origine-ngx-apps-services 0.2.1 → 0.2.3

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
  }
@@ -281,6 +285,12 @@ const DAYS = [
281
285
  { id: 'sun', label: 'Sunday' }, { id: 'mon', label: 'Monday' }, { id: 'tue', label: 'Tuesday' }, { id: 'wed', label: 'Wednesday' },
282
286
  { id: 'thu', label: 'Thursday' }, { id: 'fri', label: 'Friday' }, { id: 'sat', label: 'Saturday' },
283
287
  ];
288
+ /** While the browser tab is hidden, the page's polls slow to once a minute (the cell is read for each). */
289
+ const HIDDEN_POLL_MS = 60_000;
290
+ /** The delay before the next poll: `visibleMs` while the page is seen, once a minute while it is hidden. */
291
+ function pollDelay(visibleMs) {
292
+ return typeof document !== 'undefined' && document.visibilityState === 'hidden' ? Math.max(visibleMs, HIDDEN_POLL_MS) : visibleMs;
293
+ }
284
294
 
285
295
  // /{org}/{project}/services (prototype "services_list"): the project's instances with their state,
286
296
  // what they are attached to, their backups and price, and the organization's quota line (SVC-01, 26).
@@ -396,7 +406,7 @@ class ServicesListPage {
396
406
  </div>
397
407
  <div class="o-g2">
398
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>
399
- <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>
400
410
  </div>
401
411
  <span class="o-faint o-sm">Lifecycle states: {{ lifecycle }}. Open-source engines only.</span>
402
412
  </div>
@@ -446,7 +456,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
446
456
  </div>
447
457
  <div class="o-g2">
448
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>
449
- <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>
450
460
  </div>
451
461
  <span class="o-faint o-sm">Lifecycle states: {{ lifecycle }}. Open-source engines only.</span>
452
462
  </div>
@@ -490,7 +500,9 @@ class ServicesNewPage {
490
500
  return '3-node cluster';
491
501
  if (e?.ha === 'standby')
492
502
  return e.id === 'mysql' ? 'Standby with automatic failover (3 nodes: MOCO runs 1, 3 or 5)' : 'Standby node with automatic failover';
493
- 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`;
494
506
  }, ...(ngDevMode ? [{ debugName: "haLabel" }] : /* istanbul ignore next */ []));
495
507
  this.keep = signal(7, ...(ngDevMode ? [{ debugName: "keep" }] : /* istanbul ignore next */ []));
496
508
  this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
@@ -1288,7 +1300,7 @@ class ServiceLogsTab {
1288
1300
  this.lines.set(l.lines ?? []);
1289
1301
  this.note.set(l.message ?? '');
1290
1302
  this.loaded.set(true);
1291
- this.timer = setTimeout(() => this.load(p, id, k), 10_000);
1303
+ this.timer = setTimeout(() => this.load(p, id, k), pollDelay(10_000));
1292
1304
  },
1293
1305
  error: (e) => {
1294
1306
  this.loaded.set(true);
@@ -1595,7 +1607,7 @@ class ServicePage {
1595
1607
  this.conn = computed(() => this.revealed()?.connection ?? this.d()?.connection ?? null, ...(ngDevMode ? [{ debugName: "conn" }] : /* istanbul ignore next */ []));
1596
1608
  this.snippetKeys = computed(() => {
1597
1609
  const s = this.conn()?.snippets ?? {};
1598
- 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);
1599
1611
  }, ...(ngDevMode ? [{ debugName: "snippetKeys" }] : /* istanbul ignore next */ []));
1600
1612
  this.backups = computed(() => [...(this.d()?.backups ?? [])].sort((a, b) => (b.startedAt ?? '9').localeCompare(a.startedAt ?? '9')), ...(ngDevMode ? [{ debugName: "backups" }] : /* istanbul ignore next */ []));
1601
1613
  this.attEnvs = computed(() => {
@@ -1688,7 +1700,19 @@ class ServicePage {
1688
1700
  this.api.logs(this.key(), d, 'slow', 20).pipe(catchError(() => of(null))).subscribe((l) => this.slowLines.set(l?.lines ?? []));
1689
1701
  });
1690
1702
  });
1703
+ // Hidden, the polls slow to once a minute; seen again, the figures are read at once.
1704
+ const seen = () => {
1705
+ if (document.visibilityState !== 'visible' || !this.d())
1706
+ return;
1707
+ this.load();
1708
+ if (this.tab() !== 'overview' && this.tab() !== 'metrics')
1709
+ return;
1710
+ clearTimeout(this.metricsTimer);
1711
+ this.pollMetrics();
1712
+ };
1713
+ document.addEventListener('visibilitychange', seen);
1691
1714
  inject(DestroyRef).onDestroy(() => {
1715
+ document.removeEventListener('visibilitychange', seen);
1692
1716
  params.unsubscribe();
1693
1717
  query.unsubscribe();
1694
1718
  clearTimeout(this.loadTimer);
@@ -1729,7 +1753,7 @@ class ServicePage {
1729
1753
  this.pitr.set(toLocalInput(new Date().toISOString()));
1730
1754
  }
1731
1755
  const settling = moving(d.state) || d.readyNodes < d.nodes || (d.dumps ?? []).some((x) => x.phase !== 'completed' && x.phase !== 'failed') || !!this.openRotation() || d.attachments.some((a) => a.state === 'pending') || (d.backups ?? []).some((b) => b.phase === 'running' || b.phase === 'pending');
1732
- this.loadTimer = setTimeout(() => this.load(), settling ? 4000 : 30_000);
1756
+ this.loadTimer = setTimeout(() => this.load(), pollDelay(settling ? 4000 : 30_000));
1733
1757
  },
1734
1758
  error: (e) => {
1735
1759
  this.error.set(e?.status === 404 ? 'No such service in this project.' : apiMessage(e, 'The service could not be loaded.'));
@@ -1743,9 +1767,9 @@ class ServicePage {
1743
1767
  this.api.metrics(this.key(), d.id).subscribe({
1744
1768
  next: (m) => {
1745
1769
  this.readings.update((rs) => [...rs, m].slice(-90));
1746
- this.metricsTimer = setTimeout(() => this.pollMetrics(), 10_000);
1770
+ this.metricsTimer = setTimeout(() => this.pollMetrics(), pollDelay(10_000));
1747
1771
  },
1748
- error: () => (this.metricsTimer = setTimeout(() => this.pollMetrics(), 30_000)),
1772
+ error: () => (this.metricsTimer = setTimeout(() => this.pollMetrics(), pollDelay(30_000))),
1749
1773
  });
1750
1774
  }
1751
1775
  setTab(t) {
@@ -1756,7 +1780,7 @@ class ServicePage {
1756
1780
  return `${cpu} / ${ram} / ${d.diskGb} GB`;
1757
1781
  }
1758
1782
  snippetLabel(k) {
1759
- 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;
1760
1784
  }
1761
1785
  attState(a) {
1762
1786
  return a.state === 'live' ? 'Injected' : a.state === 'failed' ? 'Failed' : 'Redeploying…';
@@ -1885,13 +1909,14 @@ class ServicePage {
1885
1909
  return `${d.name}-restore-1`.slice(0, 40);
1886
1910
  }
1887
1911
  askRestore(d, b) {
1888
- 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 });
1889
1914
  }
1890
1915
  askPitr(d) {
1891
1916
  this.dialog.set({ kind: 'restore', name: this.restoreName(d), point: this.pitr(), backup: '' });
1892
1917
  }
1893
1918
  restore(d, dl) {
1894
- 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 };
1895
1920
  this.busy.set(true);
1896
1921
  this.error.set('');
1897
1922
  this.api.restore(this.key(), d.id, body).subscribe({
@@ -1946,6 +1971,35 @@ class ServicePage {
1946
1971
  refresh() {
1947
1972
  this.load();
1948
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
+ }
1949
2003
  snapshotName(d) {
1950
2004
  return d.engine === 'nats' ? 'stream snapshot' : d.engine === 'rabbitmq' ? 'definitions export' : 'RDB snapshot';
1951
2005
  }
@@ -2016,6 +2070,7 @@ class ServicePage {
2016
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>
2017
2071
  </div>
2018
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>}
2019
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>}
2020
2075
  @if (d.canManage && !d.deletedAt) {<button type="button" class="o-btn" (click)="setTab('settings')">Resize</button>}
2021
2076
  <a class="o-btn" [routerLink]="host.link('dashboards')">Dashboard</a>
@@ -2171,7 +2226,7 @@ class ServicePage {
2171
2226
  @for (b of backups(); track b.name) {
2172
2227
  <div class="o-row" [attr.data-testid]="'svc-backup-' + b.name">
2173
2228
  <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>
2174
- <span class="o-chip">{{ kind(b) }}</span><span class="o-faint o-sm">{{ keepLine(b, d.retentionDays) }}</span>
2229
+ <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>
2175
2230
  @if (b.phase === 'completed' && d.canManage) {<button type="button" class="o-lnk o-r" (click)="askRestore(d, b)">Restore</button>}
2176
2231
  @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>}
2177
2232
  </div>
@@ -2187,7 +2242,8 @@ class ServicePage {
2187
2242
  <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>
2188
2243
  </div>
2189
2244
  } @else {
2190
- <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>
2245
+ <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>
2246
+ @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>
2191
2247
  }
2192
2248
  <div class="o-card o-cb o-form">
2193
2249
  <b>Schedule</b>
@@ -2242,9 +2298,18 @@ class ServicePage {
2242
2298
  <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>
2243
2299
  <div class="o-cf">Each node is billed at the plan's price · {{ nodesPrice(d) }}/mo now</div>
2244
2300
  }
2301
+ @case ('replica') {
2302
+ <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>
2303
+ <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>
2304
+ @if (d.canManage) {
2305
+ <button type="button" class="o-btn o-xs" (click)="topology(d, d.standby, d.replicas - 1)" [disabled]="busy() || d.replicas === 0">−</button>
2306
+ <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>
2307
+ }</div>
2308
+ <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>
2309
+ }
2245
2310
  @default {
2246
- <div class="o-row"><span style="flex:1">Replica with failover</span><b class="o-ph-tag o-later">Later</b></div>
2247
- <div class="o-cf">{{ d.engineName }} runs one node for now; daily snapshots keep its data.</div>
2311
+ <div class="o-row"><span style="flex:1">One node</span></div>
2312
+ <div class="o-cf">{{ d.engineName }} runs one node; daily snapshots keep its data.</div>
2248
2313
  }
2249
2314
  }
2250
2315
  </div>
@@ -2299,6 +2364,7 @@ class ServicePage {
2299
2364
  }
2300
2365
  @case ('restore') {
2301
2366
  <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>
2367
+ @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>}
2302
2368
  <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>
2303
2369
  }
2304
2370
  @case ('rotate') {
@@ -2339,6 +2405,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2339
2405
  <div class="o-flex"><h1 class="o-t">{{ d.name }}</h1><span [class]="chip(d.state)" data-testid="svc-state">{{ d.state }}</span></div>
2340
2406
  </div>
2341
2407
  <div class="o-acts">
2408
+ @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>}
2342
2409
  @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>}
2343
2410
  @if (d.canManage && !d.deletedAt) {<button type="button" class="o-btn" (click)="setTab('settings')">Resize</button>}
2344
2411
  <a class="o-btn" [routerLink]="host.link('dashboards')">Dashboard</a>
@@ -2494,7 +2561,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2494
2561
  @for (b of backups(); track b.name) {
2495
2562
  <div class="o-row" [attr.data-testid]="'svc-backup-' + b.name">
2496
2563
  <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>
2497
- <span class="o-chip">{{ kind(b) }}</span><span class="o-faint o-sm">{{ keepLine(b, d.retentionDays) }}</span>
2564
+ <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>
2498
2565
  @if (b.phase === 'completed' && d.canManage) {<button type="button" class="o-lnk o-r" (click)="askRestore(d, b)">Restore</button>}
2499
2566
  @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>}
2500
2567
  </div>
@@ -2510,7 +2577,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2510
2577
  <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>
2511
2578
  </div>
2512
2579
  } @else {
2513
- <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>
2580
+ <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>
2581
+ @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>
2514
2582
  }
2515
2583
  <div class="o-card o-cb o-form">
2516
2584
  <b>Schedule</b>
@@ -2565,9 +2633,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2565
2633
  <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>
2566
2634
  <div class="o-cf">Each node is billed at the plan's price · {{ nodesPrice(d) }}/mo now</div>
2567
2635
  }
2636
+ @case ('replica') {
2637
+ <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>
2638
+ <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>
2639
+ @if (d.canManage) {
2640
+ <button type="button" class="o-btn o-xs" (click)="topology(d, d.standby, d.replicas - 1)" [disabled]="busy() || d.replicas === 0">−</button>
2641
+ <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>
2642
+ }</div>
2643
+ <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>
2644
+ }
2568
2645
  @default {
2569
- <div class="o-row"><span style="flex:1">Replica with failover</span><b class="o-ph-tag o-later">Later</b></div>
2570
- <div class="o-cf">{{ d.engineName }} runs one node for now; daily snapshots keep its data.</div>
2646
+ <div class="o-row"><span style="flex:1">One node</span></div>
2647
+ <div class="o-cf">{{ d.engineName }} runs one node; daily snapshots keep its data.</div>
2571
2648
  }
2572
2649
  }
2573
2650
  </div>
@@ -2622,6 +2699,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2622
2699
  }
2623
2700
  @case ('restore') {
2624
2701
  <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>
2702
+ @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>}
2625
2703
  <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>
2626
2704
  }
2627
2705
  @case ('rotate') {
@@ -2659,5 +2737,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2659
2737
  * Generated bundle index. Do not edit.
2660
2738
  */
2661
2739
 
2662
- export { DAYS, ENGINE_COLOUR, LIFECYCLE, PRESETS, ServiceConsoleTab, ServiceDumpsCard, ServiceLogsTab, ServiceMetricsTab, ServicePage, ServicesApi, ServicesHost, ServicesListPage, ServicesNewPage, apiMessage, backupKind, backupsLine, bytes, fromLocalInput, hhmm, hhmmss, money, moving, num, pct, presetChip, ratio, shortDate, spark, sparkPath, stateChip, stateDot, toLocalInput, utcHour, when };
2740
+ export { DAYS, ENGINE_COLOUR, HIDDEN_POLL_MS, LIFECYCLE, PRESETS, ServiceConsoleTab, ServiceDumpsCard, ServiceLogsTab, ServiceMetricsTab, ServicePage, ServicesApi, ServicesHost, ServicesListPage, ServicesNewPage, apiMessage, backupKind, backupsLine, bytes, fromLocalInput, hhmm, hhmmss, money, moving, num, pct, pollDelay, presetChip, ratio, shortDate, spark, sparkPath, stateChip, stateDot, toLocalInput, utcHour, when };
2663
2741
  //# sourceMappingURL=chisquare-tech-origine-ngx-apps-services.mjs.map