Add emergency repository unlock

This commit is contained in:
Mikei386
2026-06-21 13:59:10 +02:00
parent 84df215ad0
commit 92ba5ce05f
11 changed files with 68 additions and 9 deletions
-1
View File
@@ -1 +0,0 @@
e7a53aae8a052d41025771b79811176c9570f6ac767734e8ef77fa24fcea0bca urbm-2026.06.15.r023-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
28f348211cf8437f8227303eb0c697e4846048c8cad5b51bf9c26891b271684e urbm-2026.06.21.r001-x86_64-1.txz
+7 -2
View File
@@ -2,13 +2,18 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.15.r023"> <!ENTITY version "2026.06.21.r001">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "e7a53aae8a052d41025771b79811176c9570f6ac767734e8ef77fa24fcea0bca"> <!ENTITY packageSHA256 "28f348211cf8437f8227303eb0c697e4846048c8cad5b51bf9c26891b271684e">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.06.21.r001
- Add a separate emergency repository unlock action that invokes Restic unlock with --remove-all after a strong confirmation.
- Keep the normal unlock action safe by removing only stale locks that Restic considers safe to clear.
- Clarify unlock tooltips and warnings for repositories shared with other URBM or Restic clients.
### 2026.06.15.r023 ### 2026.06.15.r023
- Add a per-job Restic file read concurrency setting for faster backups from SSD and NVMe sources. - Add a per-job Restic file read concurrency setting for faster backups from SSD and NVMe sources.
- Keep zero as the backward-compatible Restic default and validate custom values from 1 through 64. - Keep zero as the backward-compatible Restic default and validate custom values from 1 through 64.
+10
View File
@@ -46,6 +46,7 @@ func New(socket string, svc *service.Service, log *slog.Logger) *Server {
mux.HandleFunc("POST /v1/repositories/{id}/test", s.testRepository) mux.HandleFunc("POST /v1/repositories/{id}/test", s.testRepository)
mux.HandleFunc("POST /v1/repositories/{id}/init", s.initRepository) mux.HandleFunc("POST /v1/repositories/{id}/init", s.initRepository)
mux.HandleFunc("POST /v1/repositories/{id}/unlock", s.unlockRepository) mux.HandleFunc("POST /v1/repositories/{id}/unlock", s.unlockRepository)
mux.HandleFunc("POST /v1/repositories/{id}/force-unlock", s.forceUnlockRepository)
mux.HandleFunc("POST /v1/repositories/{id}/password", s.changeRepositoryPassword) mux.HandleFunc("POST /v1/repositories/{id}/password", s.changeRepositoryPassword)
mux.HandleFunc("POST /v1/repositories/{id}/password/adopt", s.adoptRepositoryPassword) mux.HandleFunc("POST /v1/repositories/{id}/password/adopt", s.adoptRepositoryPassword)
mux.HandleFunc("POST /v1/repositories/{id}/{action}", s.maintenance) mux.HandleFunc("POST /v1/repositories/{id}/{action}", s.maintenance)
@@ -194,6 +195,15 @@ func (s *Server) unlockRepository(w http.ResponseWriter, r *http.Request) {
} }
writeJSON(w, 200, map[string]bool{"ok": true}) writeJSON(w, 200, map[string]bool{"ok": true})
} }
func (s *Server) forceUnlockRepository(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
defer cancel()
if err := s.service.ForceUnlockRepository(ctx, r.PathValue("id")); err != nil {
writeError(w, err)
return
}
writeJSON(w, 200, map[string]bool{"ok": true})
}
func (s *Server) changeRepositoryPassword(w http.ResponseWriter, r *http.Request) { func (s *Server) changeRepositoryPassword(w http.ResponseWriter, r *http.Request) {
s.repositoryPasswordAction(w, r, false) s.repositoryPasswordAction(w, r, false)
} }
+4
View File
@@ -106,6 +106,10 @@ func (r *Runner) Unlock(ctx context.Context, repo model.Repository) error {
return r.run(ctx, repo, []string{"unlock"}, nil, nil) return r.run(ctx, repo, []string{"unlock"}, nil, nil)
} }
func (r *Runner) ForceUnlock(ctx context.Context, repo model.Repository) error {
return r.run(ctx, repo, []string{"unlock", "--remove-all"}, nil, nil)
}
func (r *Runner) ChangePassword(ctx context.Context, repo model.Repository, currentPassword, newPassword string) error { func (r *Runner) ChangePassword(ctx context.Context, repo model.Repository, currentPassword, newPassword string) error {
newPasswordPath, cleanup, err := r.writePasswordFile("restic-new-password-*", newPassword) newPasswordPath, cleanup, err := r.writePasswordFile("restic-new-password-*", newPassword)
if err != nil { if err != nil {
+19
View File
@@ -343,6 +343,25 @@ func TestUnlockUsesSafeResticCommand(t *testing.T) {
} }
} }
func TestForceUnlockRemovesAllRepositoryLocks(t *testing.T) {
dir := t.TempDir()
argsPath := filepath.Join(dir, "args")
script := filepath.Join(dir, "restic")
body := fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' \"$@\" > '%s'\n", argsPath)
if err := os.WriteFile(script, []byte(body), 0700); err != nil {
t.Fatal(err)
}
runner := &Runner{Binary: script, RuntimeDir: dir, Secrets: fakeSecrets{"password": "secret"}}
repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo", PasswordRef: "password"}
if err := runner.ForceUnlock(context.Background(), repo); err != nil {
t.Fatal(err)
}
args, _ := os.ReadFile(argsPath)
if !strings.Contains(string(args), "unlock\n--remove-all") {
t.Fatalf("force unlock did not use remove-all: %s", args)
}
}
func TestResticErrorExplainsExistingRepository(t *testing.T) { func TestResticErrorExplainsExistingRepository(t *testing.T) {
err := resticError("Fatal: create repository failed: config file already exists", fmt.Errorf("exit status 1")) err := resticError("Fatal: create repository failed: config file already exists", fmt.Errorf("exit status 1"))
if !strings.Contains(err.Error(), "already initialized") || !strings.Contains(err.Error(), "select Test") { if !strings.Contains(err.Error(), "already initialized") || !strings.Contains(err.Error(), "select Test") {
+13
View File
@@ -414,6 +414,19 @@ func (s *Service) UnlockRepository(ctx context.Context, repoID string) error {
return s.restic.Unlock(ctx, mounted.Repository) return s.restic.Unlock(ctx, mounted.Repository)
} }
func (s *Service) ForceUnlockRepository(ctx context.Context, repoID string) error {
repo, ok := s.repository(repoID)
if !ok {
return errors.New("validation: unknown repository")
}
mounted, err := s.mounts.Prepare(ctx, repo)
if err != nil {
return err
}
defer s.mounts.Cleanup(context.Background(), mounted)
return s.restic.ForceUnlock(ctx, mounted.Repository)
}
func (s *Service) execute(ctx context.Context, run model.Run) model.Run { func (s *Service) execute(ctx context.Context, run model.Run) model.Run {
ctx = restic.WithRunID(ctx, run.ID) ctx = restic.WithRunID(ctx, run.ID)
if run.TaskType == "backup" { if run.TaskType == "backup" {
+6 -1
View File
@@ -2,13 +2,18 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.15.r023"> <!ENTITY version "2026.06.21.r001">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE"> <!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.06.21.r001
- Add a separate emergency repository unlock action that invokes Restic unlock with --remove-all after a strong confirmation.
- Keep the normal unlock action safe by removing only stale locks that Restic considers safe to clear.
- Clarify unlock tooltips and warnings for repositories shared with other URBM or Restic clients.
### 2026.06.15.r023 ### 2026.06.15.r023
- Add a per-job Restic file read concurrency setting for faster backups from SSD and NVMe sources. - Add a per-job Restic file read concurrency setting for faster backups from SSD and NVMe sources.
- Keep zero as the backward-compatible Restic default and validate custom values from 1 through 64. - Keep zero as the backward-compatible Restic default and validate custom values from 1 through 64.
+4 -4
View File
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
<?php <?php
$pluginRoot = '/plugins/urbm'; $pluginRoot = '/plugins/urbm';
?> ?>
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260615r023"> <link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260621r001">
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php"> <div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
<header class="bu-header"> <header class="bu-header">
<div class="bu-brand"> <div class="bu-brand">
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260615r023" alt="URBM-Logo"> <img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260621r001" alt="URBM-Logo">
<div><h1>URBM <span class="bu-version">2026.06.15.r023</span></h1><p>Restic Backup Manager für Unraid</p></div> <div><h1>URBM <span class="bu-version">2026.06.21.r001</span></h1><p>Restic Backup Manager für Unraid</p></div>
</div> </div>
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div> <div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div>
</header> </header>
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div> <div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
</div> </div>
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script> <script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.15.r023.js"></script> <script src="<?= $pluginRoot ?>/assets/urbm-2026.06.21.r001.js"></script>
+4 -1
View File
@@ -64,6 +64,7 @@
'adopt-repo-password': 'Prüft ein bereits gültiges Repository-Passwort und speichert es nur auf diesem URBM-Server, ohne Restic-Keys zu verändern.', 'adopt-repo-password': 'Prüft ein bereits gültiges Repository-Passwort und speichert es nur auf diesem URBM-Server, ohne Restic-Keys zu verändern.',
'init-repo': 'Initialisiert am Ziel einmalig ein neues, leeres Restic-Repository. Bei einem vorhandenen Repository nur das ursprüngliche Passwort eintragen und Test verwenden.', 'init-repo': 'Initialisiert am Ziel einmalig ein neues, leeres Restic-Repository. Bei einem vorhandenen Repository nur das ursprüngliche Passwort eintragen und Test verwenden.',
'unlock-repo': 'Entfernt ausschließlich von Restic als veraltet erkannte Repository-Sperren. Aktive Sperren werden nicht gelöscht.', 'unlock-repo': 'Entfernt ausschließlich von Restic als veraltet erkannte Repository-Sperren. Aktive Sperren werden nicht gelöscht.',
'force-unlock-repo': 'Notfallaktion: entfernt alle Restic-Sperren dieses Repositorys. Nur verwenden, wenn sicher kein Backup, Restore, Check oder Prune auf dieses Repository zugreift.',
'load-snapshots': 'Lädt alle Sicherungsstände des ausgewählten Repositorys.', 'load-snapshots': 'Lädt alle Sicherungsstände des ausgewählten Repositorys.',
'restore-selected': 'Übernimmt die markierten Dateien oder Ordner in den Wiederherstellungsassistenten.', 'restore-selected': 'Übernimmt die markierten Dateien oder Ordner in den Wiederherstellungsassistenten.',
'submit-restore': 'Stellt die Wiederherstellung in die globale Warteschlange; Backups und Wiederherstellungen laufen nacheinander.', 'submit-restore': 'Stellt die Wiederherstellung in die globale Warteschlange; Backups und Wiederherstellungen laufen nacheinander.',
@@ -150,6 +151,7 @@
'test-repo': 'Prüft Erreichbarkeit, Passwort und Restic-Zugriff, ohne ein Backup zu starten.', 'test-repo': 'Prüft Erreichbarkeit, Passwort und Restic-Zugriff, ohne ein Backup zu starten.',
'init-repo': 'Erstellt einmalig ein neues Restic-Repository. Nicht erneut ausführen und nicht für vorhandene Restic-Repositories verwenden.', 'init-repo': 'Erstellt einmalig ein neues Restic-Repository. Nicht erneut ausführen und nicht für vorhandene Restic-Repositories verwenden.',
'unlock-repo': 'Entfernt verwaiste Restic-Locks, etwa nach einem Neustart oder abgebrochenen Prozess. Aktive Locks bleiben erhalten.', 'unlock-repo': 'Entfernt verwaiste Restic-Locks, etwa nach einem Neustart oder abgebrochenen Prozess. Aktive Locks bleiben erhalten.',
'force-unlock-repo': 'Notfallaktion gegen hängende Locks. Entfernt alle Restic-Locks und darf nur genutzt werden, wenn wirklich kein Vorgang mehr läuft.',
'repo-snapshots': 'Wechselt zu den Sicherungsständen dieses Repositorys.', 'repo-snapshots': 'Wechselt zu den Sicherungsständen dieses Repositorys.',
'maint': action.includes(':check:') ? 'Prüft Struktur und Integrität des Restic-Repositorys.' : 'Entfernt nach Retention nicht mehr benötigte Datenblöcke und gibt Speicher frei.', 'maint': action.includes(':check:') ? 'Prüft Struktur und Integrität des Restic-Repositorys.' : 'Entfernt nach Retention nicht mehr benötigte Datenblöcke und gibt Speicher frei.',
'edit-repo': 'Öffnet Ziel-, Mount- und Zugangseinstellungen dieses Repositorys.', 'edit-repo': 'Öffnet Ziel-, Mount- und Zugangseinstellungen dieses Repositorys.',
@@ -544,7 +546,7 @@
function renderRepositories() { function renderRepositories() {
const rows = state.config.repositories.map(r => { const rows = state.config.repositories.map(r => {
const initialize = volatileLocation(r) ? disabledButton('Initialisieren','Dieses Ziel liegt im flüchtigen RAM. Zuerst über Bearbeiten einen dauerhaften Pfad wie /mnt/user/Backups/HomeServer eintragen.') : button('Initialisieren',`init-repo:${r.id}`,'primary'); const initialize = volatileLocation(r) ? disabledButton('Initialisieren','Dieses Ziel liegt im flüchtigen RAM. Zuerst über Bearbeiten einen dauerhaften Pfad wie /mnt/user/Backups/HomeServer eintragen.') : button('Initialisieren',`init-repo:${r.id}`,'primary');
const actions = initialize+button('Testen',`test-repo:${r.id}`)+button('Passwort ändern',`change-repo-password:${r.id}`)+button('Entsperren',`unlock-repo:${r.id}`)+button('Snapshots',`repo-snapshots:${r.id}`)+button('Prüfen',`maint:check:${r.id}`)+button('Bereinigen',`maint:prune:${r.id}`)+button('Bearbeiten',`edit-repo:${r.id}`)+button('Löschen',`delete-repo:${r.id}`,'danger'); const actions = initialize+button('Testen',`test-repo:${r.id}`)+button('Passwort ändern',`change-repo-password:${r.id}`)+button('Entsperren',`unlock-repo:${r.id}`)+button('Notfall entsperren',`force-unlock-repo:${r.id}`,'danger')+button('Snapshots',`repo-snapshots:${r.id}`)+button('Prüfen',`maint:check:${r.id}`)+button('Bereinigen',`maint:prune:${r.id}`)+button('Bearbeiten',`edit-repo:${r.id}`)+button('Löschen',`delete-repo:${r.id}`,'danger');
return `<tr><td><strong>${esc(r.name)}</strong><br><span class="bu-muted">${esc(r.id)}</span></td><td>${esc(typeLabels[r.type]||r.type)}</td><td>${esc(r.location)}${volatileLocation(r)?'<span class="bu-warning">Flüchtiger Pfad: Die Daten gehen beim Neustart verloren. Verwende /mnt/user/...</span>':''}</td><td>${r.mount?.managed?'Durch URBM':'Direkt / extern'}</td><td>${actionGroup(actions)}</td></tr>`; return `<tr><td><strong>${esc(r.name)}</strong><br><span class="bu-muted">${esc(r.id)}</span></td><td>${esc(typeLabels[r.type]||r.type)}</td><td>${esc(r.location)}${volatileLocation(r)?'<span class="bu-warning">Flüchtiger Pfad: Die Daten gehen beim Neustart verloren. Verwende /mnt/user/...</span>':''}</td><td>${r.mount?.managed?'Durch URBM':'Direkt / extern'}</td><td>${actionGroup(actions)}</td></tr>`;
}).join(''); }).join('');
return `<div class="bu-toolbar"><div><h2>Repositorys</h2><div class="bu-muted">Lokale, SMB-, NFS- und SFTP-Ziele.</div></div><div class="bu-actions">${button('Vorhandenes einbinden','existing-repo')}${button('Neues Repository','new-repo','primary')}</div></div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Typ</th><th>Speicherort</th><th>Einbindung</th><th>Aktionen</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">Keine Repositorys konfiguriert.</div>'}</section>`; return `<div class="bu-toolbar"><div><h2>Repositorys</h2><div class="bu-muted">Lokale, SMB-, NFS- und SFTP-Ziele.</div></div><div class="bu-actions">${button('Vorhandenes einbinden','existing-repo')}${button('Neues Repository','new-repo','primary')}</div></div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Typ</th><th>Speicherort</th><th>Einbindung</th><th>Aktionen</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">Keine Repositorys konfiguriert.</div>'}</section>`;
@@ -699,6 +701,7 @@
if (name === 'test-repo') { await api(`/v1/repositories/${a}/test`,{method:'POST'}); return notify('Repository-Verbindung erfolgreich getestet'); } if (name === 'test-repo') { await api(`/v1/repositories/${a}/test`,{method:'POST'}); return notify('Repository-Verbindung erfolgreich getestet'); }
if (name === 'init-repo') { if(confirm('Hier ein NEUES Restic-Repository erstellen?\n\nNur fortfahren, wenn dieses Ziel noch kein Restic-Repository enthält. Bei einem vorhandenen Repository das ursprüngliche Passwort unter Bearbeiten eintragen und Testen verwenden.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository erfolgreich initialisiert'); } return; } if (name === 'init-repo') { if(confirm('Hier ein NEUES Restic-Repository erstellen?\n\nNur fortfahren, wenn dieses Ziel noch kein Restic-Repository enthält. Bei einem vorhandenen Repository das ursprüngliche Passwort unter Bearbeiten eintragen und Testen verwenden.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository erfolgreich initialisiert'); } return; }
if (name === 'unlock-repo') { if(confirm('Verwaiste Restic-Sperren dieses Repositorys entfernen?\n\nRestic entfernt dabei nur als veraltet erkannte Locks. Verwende diese Aktion nach einem Neustart oder abgebrochenen Restic-Prozess.')) { await api(`/v1/repositories/${a}/unlock`,{method:'POST'}); notify('Verwaiste Repository-Sperren wurden entfernt'); } return; } if (name === 'unlock-repo') { if(confirm('Verwaiste Restic-Sperren dieses Repositorys entfernen?\n\nRestic entfernt dabei nur als veraltet erkannte Locks. Verwende diese Aktion nach einem Neustart oder abgebrochenen Restic-Prozess.')) { await api(`/v1/repositories/${a}/unlock`,{method:'POST'}); notify('Verwaiste Repository-Sperren wurden entfernt'); } return; }
if (name === 'force-unlock-repo') { if(confirm('NOTFALL-ENTSPERRUNG\n\nDiese Aktion entfernt ALLE Restic-Sperren dieses Repositorys.\n\nNur fortfahren, wenn wirklich kein Backup, Restore, Check oder Prune mehr läuft - auch nicht auf einem anderen Server, der dieses Repository nutzt. Bei einem laufenden Vorgang kann das Repository beschädigt oder der laufende Vorgang fehlschlagen.\n\nAlle Sperren jetzt entfernen?')) { await api(`/v1/repositories/${a}/force-unlock`,{method:'POST'}); notify('Alle Repository-Sperren wurden entfernt'); } return; }
if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a==='check'?'Prüfung':'Bereinigung'} wurde eingereiht`); return load(); } if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a==='check'?'Prüfung':'Bereinigung'} wurde eingereiht`); return load(); }
if (name === 'repo-snapshots') { state.view='snapshots'; document.querySelector('[data-view="snapshots"]').click(); render(); setTimeout(()=>{document.getElementById('bu-snapshot-repo').value=a; handle('load-snapshots');},0); return; } if (name === 'repo-snapshots') { state.view='snapshots'; document.querySelector('[data-view="snapshots"]').click(); render(); setTimeout(()=>{document.getElementById('bu-snapshot-repo').value=a; handle('load-snapshots');},0); return; }
if (name === 'load-snapshots') { const repo=document.getElementById('bu-snapshot-repo').value; state.snapshots=await api(`/v1/repositories/${repo}/snapshots`); state.snapshotRepo=repo; return render(); } if (name === 'load-snapshots') { const repo=document.getElementById('bu-snapshot-repo').value; state.snapshots=await api(`/v1/repositories/${repo}/snapshots`); state.snapshotRepo=repo; return render(); }