Merge claude/1

This commit is contained in:
librelad 2026-05-28 18:38:28 +01:00
commit f6fecd023a
3 changed files with 144 additions and 15 deletions

View File

@ -1322,6 +1322,83 @@
font-size: 0.72rem; font-size: 0.72rem;
color: rgba(var(--text-rgb), 0.7); color: rgba(var(--text-rgb), 0.7);
} }
/* Snapshot detail panel. The shared .task-meta/.meta-item layout forces
one nowrap line per item and clips long values (the full date, repo
paths) mid-string, so the backup row gets its own label-over-value grid
plus full-width blocks for tags and paths that wrap cleanly. */
.backup-snapshot-meta {
display: flex;
flex-direction: column;
gap: 14px;
}
.backup-snapshot-meta .bsm-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 14px 18px;
}
.backup-snapshot-meta .bsm-field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.backup-snapshot-meta .bsm-label {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: rgba(var(--text-rgb), 0.45);
}
.backup-snapshot-meta .bsm-value {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
font-size: 0.85rem;
color: var(--text-primary);
}
.backup-snapshot-meta .bsm-value code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.78rem;
background: rgba(var(--text-rgb), 0.06);
color: rgba(var(--text-rgb), 0.82);
padding: 1px 6px;
border-radius: 4px;
word-break: break-all;
}
.backup-snapshot-meta .bsm-block {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 13px;
border-top: 1px solid rgba(var(--text-rgb), 0.08);
}
.backup-snapshot-meta .bsm-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.backup-snapshot-meta .bsm-paths {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.backup-snapshot-meta .bsm-paths code {
display: inline-block;
max-width: 100%;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.78rem;
background: rgba(var(--text-rgb), 0.06);
color: rgba(var(--text-rgb), 0.82);
padding: 4px 9px;
border-radius: 5px;
word-break: break-all;
}
.backup-snapshot-overflow { .backup-snapshot-overflow {
margin-top: 10px; margin-top: 10px;
font-size: 0.78rem; font-size: 0.78rem;

View File

@ -91,6 +91,14 @@ class BackupAppCard {
_renderRow(s, iconUrl) { _renderRow(s, iconUrl) {
const sid = String(s.id || ''); const sid = String(s.id || '');
const tagMap = {};
(s.tags || []).forEach(t => { const i = t.indexOf('='); if (i > 0) tagMap[t.slice(0, i)] = t.slice(i + 1); });
const engineName = tagMap.engine ? tagMap.engine.charAt(0).toUpperCase() + tagMap.engine.slice(1) : null;
const otherTags = (s.tags || []).filter(t => !/^(app|host|engine|paths?)=/.test(t));
const field = (label, valueHtml) =>
`<div class="bsm-field"><span class="bsm-label">${label}</span><span class="bsm-value">${valueHtml}</span></div>`;
return ` return `
<div class="task-item backup-snapshot-item" data-snapshot="${this.escape(sid)}" data-loc="${this.escape(String(s.locIdx))}"> <div class="task-item backup-snapshot-item" data-snapshot="${this.escape(sid)}" data-loc="${this.escape(String(s.locIdx))}">
<div class="task-header"> <div class="task-header">
@ -113,13 +121,24 @@ class BackupAppCard {
</div> </div>
</div> </div>
<div class="task-details"> <div class="task-details">
<div class="task-meta"> <div class="backup-snapshot-meta">
<div class="meta-item"><strong>Backup ID:</strong> <code>${this.escape(sid)}</code></div> <div class="bsm-grid">
<div class="meta-item"><strong>Location:</strong> ${this.escape(s.locName)}</div> ${s.hostname ? field('Host', this.escape(s.hostname)) : ''}
<div class="meta-item"><strong>When:</strong> ${this.escape(this._fmtFull(s.time))}</div> ${field('Location', `<span class="backup-snapshot-loc-pill">${this.escape(s.locName)}</span>`)}
${s.hostname ? `<div class="meta-item"><strong>Host:</strong> ${this.escape(s.hostname)}</div>` : ''} ${field('Backup ID', `<code>${this.escape(sid)}</code>`)}
${s.tags && s.tags.length ? `<div class="meta-item"><strong>Tags:</strong> ${s.tags.map(t => `<span class="backup-snapshot-tag">${this.escape(t)}</span>`).join(' ')}</div>` : ''} ${field('When', `<span title="${this.escape(this._fmtFull(s.time))}">${this.escape(this._fmtNice(s.time))}</span>`)}
${s.paths && s.paths.length ? `<div class="meta-item"><strong>Paths:</strong><br><code>${s.paths.map(p => this.escape(p)).join('<br>')}</code></div>` : ''} ${engineName ? field('Engine', this.escape(engineName)) : ''}
</div>
${otherTags.length ? `
<div class="bsm-block">
<span class="bsm-label">Tags</span>
<div class="bsm-tags">${otherTags.map(t => `<span class="backup-snapshot-tag">${this.escape(t)}</span>`).join('')}</div>
</div>` : ''}
${s.paths && s.paths.length ? `
<div class="bsm-block">
<span class="bsm-label">Paths</span>
<ul class="bsm-paths">${s.paths.map(p => `<li><code>${this.escape(p)}</code></li>`).join('')}</ul>
</div>` : ''}
</div> </div>
</div> </div>
</div>`; </div>`;
@ -151,6 +170,12 @@ class BackupAppCard {
if (isNaN(d.getTime())) return iso; if (isNaN(d.getTime())) return iso;
return d.toString(); return d.toString();
} }
_fmtNice(iso) {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
}
async loadData() { async loadData() {
const ts = Date.now(); const ts = Date.now();

View File

@ -1154,6 +1154,16 @@ class BackupPage {
? `<a class="backup-snapshot-link backup-snapshot-app-chip" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}" title="Open ${this.escape(displayName)} backups">${this.escape(displayName)}</a>` ? `<a class="backup-snapshot-link backup-snapshot-app-chip" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}" title="Open ${this.escape(displayName)} backups">${this.escape(displayName)}</a>`
: `<span class="backup-snapshot-app-chip">${this.escape(displayName)}</span>`; : `<span class="backup-snapshot-app-chip">${this.escape(displayName)}</span>`;
const sid = String(r.id); const sid = String(r.id);
// Restic stamps app=/host=/engine= into the snapshot tags; surface
// those as their own fields and keep any remaining tags as chips.
const tagMap = {};
(r.tags || []).forEach(t => { const i = t.indexOf('='); if (i > 0) tagMap[t.slice(0, i)] = t.slice(i + 1); });
const engineName = tagMap.engine ? this.engineDisplayName(tagMap.engine) : null;
const otherTags = (r.tags || []).filter(t => !/^(app|host|engine|paths?)=/.test(t));
const field = (label, valueHtml) =>
`<div class="bsm-field"><span class="bsm-label">${label}</span><span class="bsm-value">${valueHtml}</span></div>`;
return ` return `
<div class="task-item backup-snapshot-item" data-snapshot="${this.escape(sid)}" data-loc="${this.escape(String(r.locIdx))}"> <div class="task-item backup-snapshot-item" data-snapshot="${this.escape(sid)}" data-loc="${this.escape(String(r.locIdx))}">
<div class="task-header" data-action="toggle-snapshot-row"> <div class="task-header" data-action="toggle-snapshot-row">
@ -1181,14 +1191,25 @@ class BackupPage {
</div> </div>
</div> </div>
<div class="task-details"> <div class="task-details">
<div class="task-meta"> <div class="backup-snapshot-meta">
<div class="meta-item"><strong>App:</strong> ${this.escape(displayName)}${hasApp ? ` (<code>${this.escape(r.app)}</code>)` : ''}</div> <div class="bsm-grid">
<div class="meta-item"><strong>Backup ID:</strong> <code>${this.escape(sid)}</code></div> ${field('App', `${this.escape(displayName)}${hasApp ? ` <code>${this.escape(r.app)}</code>` : ''}`)}
<div class="meta-item"><strong>Location:</strong> ${this.escape(r.locName)}</div> ${field('Host', this.escape(r.host))}
<div class="meta-item"><strong>When:</strong> ${this.escape(this._fmtFullTime(r.time))}</div> ${field('Location', `<span class="backup-snapshot-loc-pill">${this.escape(r.locName)}</span>`)}
<div class="meta-item"><strong>Host:</strong> ${this.escape(r.host)}</div> ${field('Backup ID', `<code>${this.escape(sid)}</code>`)}
${r.tags && r.tags.length ? `<div class="meta-item"><strong>Tags:</strong> ${r.tags.map(t => `<span class="backup-snapshot-tag">${this.escape(t)}</span>`).join(' ')}</div>` : ''} ${field('When', `<span title="${this.escape(this._fmtFullTime(r.time))}">${this.escape(this._fmtNiceTime(r.time))}</span>`)}
${r.paths && r.paths.length ? `<div class="meta-item"><strong>Paths:</strong><br><code>${r.paths.map(p => this.escape(p)).join('<br>')}</code></div>` : ''} ${engineName ? field('Engine', this.escape(engineName)) : ''}
</div>
${otherTags.length ? `
<div class="bsm-block">
<span class="bsm-label">Tags</span>
<div class="bsm-tags">${otherTags.map(t => `<span class="backup-snapshot-tag">${this.escape(t)}</span>`).join('')}</div>
</div>` : ''}
${r.paths && r.paths.length ? `
<div class="bsm-block">
<span class="bsm-label">Paths</span>
<ul class="bsm-paths">${r.paths.map(p => `<li><code>${this.escape(p)}</code></li>`).join('')}</ul>
</div>` : ''}
</div> </div>
</div> </div>
</div>`; </div>`;
@ -1206,6 +1227,12 @@ class BackupPage {
if (isNaN(d.getTime())) return String(iso); if (isNaN(d.getTime())) return String(iso);
return d.toString(); return d.toString();
} }
_fmtNiceTime(iso) {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return String(iso);
return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
}
renderConfiguration() { renderConfiguration() {
const body = document.getElementById('backup-configuration-body'); const body = document.getElementById('backup-configuration-body');