Sort lists newest first
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
dca4b39ea845617f7789967f880f874984a8d9be85ee94b2fbea821734bd7559 urbm-2026.06.21.r005-x86_64-1.txz
|
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
03a78ede9f43f0848f0ac688f436a51f5fcd01dbf5638298c6ece7e8b18a742a urbm-2026.06.21.r006-x86_64-1.txz
|
||||||
Vendored
+6
-2
@@ -2,13 +2,17 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "urbm">
|
<!ENTITY name "urbm">
|
||||||
<!ENTITY author "Michael Roll">
|
<!ENTITY author "Michael Roll">
|
||||||
<!ENTITY version "2026.06.21.r005">
|
<!ENTITY version "2026.06.21.r006">
|
||||||
<!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 "dca4b39ea845617f7789967f880f874984a8d9be85ee94b2fbea821734bd7559">
|
<!ENTITY packageSHA256 "03a78ede9f43f0848f0ac688f436a51f5fcd01dbf5638298c6ece7e8b18a742a">
|
||||||
]>
|
]>
|
||||||
<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.r006
|
||||||
|
- Sort runs and snapshots newest-first so lists get older from top to bottom.
|
||||||
|
- Apply newest-first ordering consistently to dashboard activity, activity history, failed runs, and snapshot tables.
|
||||||
|
|
||||||
### 2026.06.21.r005
|
### 2026.06.21.r005
|
||||||
- Reduce the snapshot path-count column width so the browse action sits directly next to it.
|
- Reduce the snapshot path-count column width so the browse action sits directly next to it.
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -2,13 +2,17 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "urbm">
|
<!ENTITY name "urbm">
|
||||||
<!ENTITY author "Michael Roll">
|
<!ENTITY author "Michael Roll">
|
||||||
<!ENTITY version "2026.06.21.r005">
|
<!ENTITY version "2026.06.21.r006">
|
||||||
<!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.r006
|
||||||
|
- Sort runs and snapshots newest-first so lists get older from top to bottom.
|
||||||
|
- Apply newest-first ordering consistently to dashboard activity, activity history, failed runs, and snapshot tables.
|
||||||
|
|
||||||
### 2026.06.21.r005
|
### 2026.06.21.r005
|
||||||
- Reduce the snapshot path-count column width so the browse action sits directly next to it.
|
- Reduce the snapshot path-count column width so the browse action sits directly next to it.
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -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=20260621r005">
|
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260621r006">
|
||||||
<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=20260621r005" alt="URBM-Logo">
|
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260621r006" alt="URBM-Logo">
|
||||||
<div><h1>URBM <span class="bu-version">2026.06.21.r005</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
<div><h1>URBM <span class="bu-version">2026.06.21.r006</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.21.r005.js"></script>
|
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.21.r006.js"></script>
|
||||||
|
|||||||
+12
-7
@@ -335,7 +335,7 @@
|
|||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const [config, runs, daemon] = await Promise.all([api('/v1/config'), api('/v1/runs'), api('/v1/health')]);
|
const [config, runs, daemon] = await Promise.all([api('/v1/config'), api('/v1/runs'), api('/v1/health')]);
|
||||||
state.config = config; state.runs = runs;
|
state.config = config; state.runs = newestRuns(runs);
|
||||||
health.textContent = `Daemon online (${daemon.version || 'unbekannt'})`; health.className = 'bu-health ok'; render(); loadRepositoryStats();
|
health.textContent = `Daemon online (${daemon.version || 'unbekannt'})`; health.className = 'bu-health ok'; render(); loadRepositoryStats();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
health.textContent = 'Daemon nicht erreichbar'; health.className = 'bu-health bad';
|
health.textContent = 'Daemon nicht erreichbar'; health.className = 'bu-health bad';
|
||||||
@@ -370,8 +370,9 @@
|
|||||||
|
|
||||||
function renderDashboard() {
|
function renderDashboard() {
|
||||||
const active = state.runs.filter(r => ['queued','running','paused'].includes(r.status)).length;
|
const active = state.runs.filter(r => ['queued','running','paused'].includes(r.status)).length;
|
||||||
const failures = state.runs.filter(r => r.status === 'failed').slice(-5);
|
const orderedRuns = newestRuns(state.runs);
|
||||||
const last = [...state.runs].reverse().slice(0, 8);
|
const failures = orderedRuns.filter(r => r.status === 'failed').slice(0, 5);
|
||||||
|
const last = orderedRuns.slice(0, 8);
|
||||||
const stats=state.repositoryStats;
|
const stats=state.repositoryStats;
|
||||||
const statsErrors=stats.filter(item=>item.error);
|
const statsErrors=stats.filter(item=>item.error);
|
||||||
const loading=state.repositoryStatsLoading?'<div class="bu-muted">Repository-Werte werden geladen…</div>':statsErrors.length?`<div class="bu-warning">${statsErrors.map(item=>`${esc(item.repositoryName)}: ${esc(displayMessage(item.error))}`).join('<br>')}</div>`:'';
|
const loading=state.repositoryStatsLoading?'<div class="bu-muted">Repository-Werte werden geladen…</div>':statsErrors.length?`<div class="bu-warning">${statsErrors.map(item=>`${esc(item.repositoryName)}: ${esc(displayMessage(item.error))}`).join('<br>')}</div>`:'';
|
||||||
@@ -629,7 +630,7 @@
|
|||||||
function renderSnapshots() {
|
function renderSnapshots() {
|
||||||
const options = state.config.repositories.map(r=>`<option value="${esc(r.id)}">${esc(r.name)}</option>`).join('');
|
const options = state.config.repositories.map(r=>`<option value="${esc(r.id)}">${esc(r.name)}</option>`).join('');
|
||||||
const pathCount = paths => `<span class="bu-path-count" title="${esc((paths||[]).join('\n'))}">${Number((paths||[]).length).toLocaleString()} Pfad${(paths||[]).length===1?'':'e'}</span>`;
|
const pathCount = paths => `<span class="bu-path-count" title="${esc((paths||[]).join('\n'))}">${Number((paths||[]).length).toLocaleString()} Pfad${(paths||[]).length===1?'':'e'}</span>`;
|
||||||
const rows = state.snapshots.map(s=>`<tr><td>${esc(s.short_id||s.id)}</td><td>${esc(new Date(s.time).toLocaleString())}</td><td>${esc(s.hostname)}</td><td>${pathCount(s.paths)}</td><td>${button('Durchsuchen',`browse:${s.id}`)}</td></tr>`).join('');
|
const rows = newestSnapshots(state.snapshots).map(s=>`<tr><td>${esc(s.short_id||s.id)}</td><td>${esc(new Date(s.time).toLocaleString())}</td><td>${esc(s.hostname)}</td><td>${pathCount(s.paths)}</td><td>${button('Durchsuchen',`browse:${s.id}`)}</td></tr>`).join('');
|
||||||
return `<div class="bu-toolbar"><h2>Snapshots</h2><div class="bu-actions"><select id="bu-snapshot-repo">${options}</select>${button('Laden','load-snapshots','primary')}</div></div><section class="bu-card">${rows?`<div class="bu-table-scroll"><table class="bu-table bu-snapshot-table"><colgroup><col class="bu-snapshot-id"><col class="bu-snapshot-time"><col class="bu-snapshot-host"><col class="bu-snapshot-paths"><col class="bu-snapshot-actions"></colgroup><thead><tr><th>Snapshot</th><th>Zeitpunkt</th><th>Host</th><th>Pfade</th><th></th></tr></thead><tbody>${rows}</tbody></table></div>`:'<div class="bu-empty">Repository auswählen und Snapshots laden.</div>'}</section>${state.selectedSnapshot ? renderFileBrowser() : ''}`;
|
return `<div class="bu-toolbar"><h2>Snapshots</h2><div class="bu-actions"><select id="bu-snapshot-repo">${options}</select>${button('Laden','load-snapshots','primary')}</div></div><section class="bu-card">${rows?`<div class="bu-table-scroll"><table class="bu-table bu-snapshot-table"><colgroup><col class="bu-snapshot-id"><col class="bu-snapshot-time"><col class="bu-snapshot-host"><col class="bu-snapshot-paths"><col class="bu-snapshot-actions"></colgroup><thead><tr><th>Snapshot</th><th>Zeitpunkt</th><th>Host</th><th>Pfade</th><th></th></tr></thead><tbody>${rows}</tbody></table></div>`:'<div class="bu-empty">Repository auswählen und Snapshots laden.</div>'}</section>${state.selectedSnapshot ? renderFileBrowser() : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +691,7 @@
|
|||||||
return `<section class="bu-card"><h2>Wiederherstellen</h2><form id="bu-restore-form" class="bu-form"><label>Repository<select name="repositoryId">${options}</select></label><label>Snapshot-ID<input name="snapshotId" required value="${esc(state.selectedSnapshot||'')}"></label><label class="wide">Wiederherzustellende Pfade, einer pro Zeile<textarea name="includes">${esc((state.restoreIncludes||[]).join('\n'))}</textarea></label><label class="wide">Ziel<input name="target" required value="${esc(state.config.settings.restoreRoot)}/${esc(id('restore'))}"></label><div id="bu-dir-tree-restoreTarget" class="wide"></div><label>Am Originalort wiederherstellen<input name="inPlace" type="checkbox"></label><label>Überschreibungsrisiko bestätigen<input name="confirmed" type="checkbox"></label><div class="wide bu-actions">${button('Wiederherstellung einreihen','submit-restore','primary')}</div></form></section>`;
|
return `<section class="bu-card"><h2>Wiederherstellen</h2><form id="bu-restore-form" class="bu-form"><label>Repository<select name="repositoryId">${options}</select></label><label>Snapshot-ID<input name="snapshotId" required value="${esc(state.selectedSnapshot||'')}"></label><label class="wide">Wiederherzustellende Pfade, einer pro Zeile<textarea name="includes">${esc((state.restoreIncludes||[]).join('\n'))}</textarea></label><label class="wide">Ziel<input name="target" required value="${esc(state.config.settings.restoreRoot)}/${esc(id('restore'))}"></label><div id="bu-dir-tree-restoreTarget" class="wide"></div><label>Am Originalort wiederherstellen<input name="inPlace" type="checkbox"></label><label>Überschreibungsrisiko bestätigen<input name="confirmed" type="checkbox"></label><div class="wide bu-actions">${button('Wiederherstellung einreihen','submit-restore','primary')}</div></form></section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderActivity() { return `<div class="bu-toolbar"><div><h2>Aktivitäten</h2><div class="bu-muted">Laufende Aufgaben werden automatisch alle zwei Sekunden aktualisiert.</div></div><div class="bu-actions">${button('Aktualisieren','refresh','primary')}${button('Abgeschlossene Aktivitäten löschen','clear-activity','danger')}</div></div><section class="bu-card">${runsTable([...state.runs].reverse())}</section>`; }
|
function renderActivity() { return `<div class="bu-toolbar"><div><h2>Aktivitäten</h2><div class="bu-muted">Laufende Aufgaben werden automatisch alle zwei Sekunden aktualisiert.</div></div><div class="bu-actions">${button('Aktualisieren','refresh','primary')}${button('Abgeschlossene Aktivitäten löschen','clear-activity','danger')}</div></div><section class="bu-card">${runsTable(newestRuns(state.runs))}</section>`; }
|
||||||
function progressView(run) {
|
function progressView(run) {
|
||||||
if(run.status==='queued') return '<span class="bu-muted">Wartet in der Warteschlange</span>';
|
if(run.status==='queued') return '<span class="bu-muted">Wartet in der Warteschlange</span>';
|
||||||
if(run.taskType!=='backup'&&run.taskType!=='rsync'&&!run.progressPercent) return '<span class="bu-muted">Kein Prozentwert verfügbar</span>';
|
if(run.taskType!=='backup'&&run.taskType!=='rsync'&&!run.progressPercent) return '<span class="bu-muted">Kein Prozentwert verfügbar</span>';
|
||||||
@@ -737,6 +738,10 @@
|
|||||||
return state.config;
|
return state.config;
|
||||||
}
|
}
|
||||||
const lines = (value) => value.split('\n').map(v=>v.trim()).filter(Boolean);
|
const lines = (value) => value.split('\n').map(v=>v.trim()).filter(Boolean);
|
||||||
|
const runTime = run => new Date(run.createdAt || run.startedAt || run.finishedAt || 0).getTime() || 0;
|
||||||
|
const snapshotTime = snapshot => new Date(snapshot.time || 0).getTime() || 0;
|
||||||
|
const newestRuns = runs => [...runs].sort((a,b)=>runTime(b)-runTime(a));
|
||||||
|
const newestSnapshots = snapshots => [...snapshots].sort((a,b)=>snapshotTime(b)-snapshotTime(a));
|
||||||
|
|
||||||
async function handle(action, element) {
|
async function handle(action, element) {
|
||||||
const [name, a, b] = action.split(':');
|
const [name, a, b] = action.split(':');
|
||||||
@@ -760,7 +765,7 @@
|
|||||||
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 === '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=newestSnapshots(await api(`/v1/repositories/${repo}/snapshots`)); state.snapshotRepo=repo; return render(); }
|
||||||
if (name === 'browse') { state.selectedSnapshot=a; state.restoreIncludes=[]; state.files=[]; state.snapshotTree=null; state.snapshotTreeError=''; state.snapshotTreeExpanded={'/':true}; state.snapshotTreeLoading=true; render(); try { state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${a}/files`); state.snapshotTree=buildSnapshotTree(state.files); } catch(error) { state.snapshotTreeError=`Snapshot-Struktur konnte nicht geladen werden: ${error.message}`; } finally { state.snapshotTreeLoading=false; return render(); } }
|
if (name === 'browse') { state.selectedSnapshot=a; state.restoreIncludes=[]; state.files=[]; state.snapshotTree=null; state.snapshotTreeError=''; state.snapshotTreeExpanded={'/':true}; state.snapshotTreeLoading=true; render(); try { state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${a}/files`); state.snapshotTree=buildSnapshotTree(state.files); } catch(error) { state.snapshotTreeError=`Snapshot-Struktur konnte nicht geladen werden: ${error.message}`; } finally { state.snapshotTreeLoading=false; return render(); } }
|
||||||
if (name === 'toggle-source-tree') { return toggleSourceTree(decodeURIComponent(a)); }
|
if (name === 'toggle-source-tree') { return toggleSourceTree(decodeURIComponent(a)); }
|
||||||
if (name === 'retry-source-tree') { const path=decodeURIComponent(a); delete state.sourceTreeChildren[path]; state.sourceTreeErrors[path]=''; return toggleSourceTree(path,true); }
|
if (name === 'retry-source-tree') { const path=decodeURIComponent(a); delete state.sourceTreeChildren[path]; state.sourceTreeErrors[path]=''; return toggleSourceTree(path,true); }
|
||||||
@@ -910,6 +915,6 @@
|
|||||||
window.addEventListener('scroll', hideTooltip, true);
|
window.addEventListener('scroll', hideTooltip, true);
|
||||||
window.addEventListener('resize', hideTooltip);
|
window.addEventListener('resize', hideTooltip);
|
||||||
enhanceTooltips(root);
|
enhanceTooltips(root);
|
||||||
setInterval(async()=>{ if(!state.config)return; try { state.runs=await api('/v1/runs'); if(['dashboard','activity'].includes(state.view))render(); }catch(_){ } },2000);
|
setInterval(async()=>{ if(!state.config)return; try { state.runs=newestRuns(await api('/v1/runs')); if(['dashboard','activity'].includes(state.view))render(); }catch(_){ } },2000);
|
||||||
load();
|
load();
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user