diff --git a/web/src/components/SettingsView.tsx b/web/src/components/SettingsView.tsx index 42c64e7..f3e2986 100644 --- a/web/src/components/SettingsView.tsx +++ b/web/src/components/SettingsView.tsx @@ -399,7 +399,10 @@ export default function SettingsView({ -
+ {/* row-top: the description runs four lines, and the interval is + the thing you read it for — it belongs level with the text, not + floating halfway down beside it. */} +
Back up the bucket
@@ -443,18 +446,22 @@ export default function SettingsView({
{/* The dashboard strip says a backup is failing; this says what the Job actually reported, which is what tells you - whether it is yours to fix. */} + whether it is yours to fix. The message and the Job's own + reason are a paragraph you read once, so they hang off the + timestamp as a tooltip rather than wrapping across the + table and pushing every other row around. */} {bk.lastFailure && (
Last failure - + {new Date(bk.lastFailure.at).toLocaleString()} - {bk.lastFailure.message ? ` — ${bk.lastFailure.message}` : ''} {bk.failures > 1 ? ` (${bk.failures} in a row)` : ''} - {bk.lastFailure.reason && ( -
- {bk.lastFailure.reason} -
+ {(bk.lastFailure.message || bk.lastFailure.reason) && ( + )}
@@ -467,80 +474,6 @@ export default function SettingsView({ )}
)} - {/* On demand regardless of the interval: taking one backup - before a risky change should not mean switching on a - schedule. Disabled while a run is in flight — two Jobs - uploading to one dataset would race. */} - {/* Folders to keep out of the history. An env directory is - thousands of files the backup has to hash and none of them - worth keeping — measured, that is 7s of work versus 1s. */} - {bk?.canRunNow && ( - <> -
- - Skip these folders — type a name and press Enter. Anywhere they appear - (node_modules, .venv), - they stay out of the history. Everything here is something a command puts - back; nothing is skipped for being large. Remove any you want kept. - - {/* The default list is long and picked from measurements, so an operator - who trims it has no way back without retyping 30 names. Hidden when - the list already matches the defaults, so it is never a no-op — - compared against the local edit rather than the server's copy, or it - would linger until the next status poll. */} - {bk.excludeDefaults?.length > 0 - && (cfg.backup.exclude.length !== bk.excludeDefaults.length - || cfg.backup.exclude.some((t, i) => t !== bk.excludeDefaults[i])) && ( - - )} -
-
{ - if (e.target === e.currentTarget) (e.currentTarget.querySelector('input') as HTMLInputElement)?.focus(); - }}> - {cfg.backup.exclude.map((t) => ( - - {t} - - - ))} - setSkipDraft(e.target.value)} - onKeyDown={(e) => { - // Enter or comma commits; Backspace on an empty box - // takes the last chip back, as tag fields do. - if (e.key === 'Enter' || e.key === ',') { - e.preventDefault(); - addSkip(); - } else if (e.key === 'Backspace' && !skipDraft && cfg.backup.exclude.length) { - setCfg({ ...cfg, backup: { ...cfg.backup, exclude: cfg.backup.exclude.slice(0, -1) } }); - } - }} - onBlur={addSkip} - /> -
- - )} - {bk?.canRunNow && ( - - )} - {bkMsg &&
{bkMsg}
}
@@ -574,6 +507,84 @@ export default function SettingsView({
)} + {/* Folders to keep out of the history. An env directory is + thousands of files the backup has to hash and none of them + worth keeping — measured, that is 7s of work versus 1s. + Full width, outside the row: thirty chips wrapping inside the + label column read as a wall, and the field they belong to is a + text input, which wants the whole measure. */} + {bk?.canRunNow && ( + <> +
+ + Skip these folders — type a name and press Enter. Anywhere they appear + (node_modules, .venv), + they stay out of the history. Everything here is something a command puts + back; nothing is skipped for being large. Remove any you want kept. + + {/* The default list is long and picked from measurements, so an operator + who trims it has no way back without retyping 30 names. Hidden when + the list already matches the defaults, so it is never a no-op — + compared against the local edit rather than the server's copy, or it + would linger until the next status poll. */} + {bk.excludeDefaults?.length > 0 + && (cfg.backup.exclude.length !== bk.excludeDefaults.length + || cfg.backup.exclude.some((t, i) => t !== bk.excludeDefaults[i])) && ( + + )} +
+
{ + if (e.target === e.currentTarget) (e.currentTarget.querySelector('input') as HTMLInputElement)?.focus(); + }}> + {cfg.backup.exclude.map((t) => ( + + {t} + + + ))} + setSkipDraft(e.target.value)} + onKeyDown={(e) => { + // Enter or comma commits; Backspace on an empty box + // takes the last chip back, as tag fields do. + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + addSkip(); + } else if (e.key === 'Backspace' && !skipDraft && cfg.backup.exclude.length) { + setCfg({ ...cfg, backup: { ...cfg.backup, exclude: cfg.backup.exclude.slice(0, -1) } }); + } + }} + onBlur={addSkip} + /> +
+ + )} + {/* On demand regardless of the interval: taking one backup + before a risky change should not mean switching on a + schedule. Disabled while a run is in flight — two Jobs + uploading to one dataset would race. */} + {bk?.canRunNow && ( + + )} + {bkMsg &&
{bkMsg}
} +
Archive inactive sessions
diff --git a/web/src/styles.css b/web/src/styles.css index 009aa7c..7acd340 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -871,6 +871,9 @@ body { .settings-page h3 { margin: 24px 0 8px; font-size: 14px; } /* plain rows, not cards: the bold label is the section header */ .setting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 2px; } +/* a row whose description runs several lines: the control sits level with the + text instead of centring against it, so the eye finds it where it starts */ +.setting-row.row-top { align-items: flex-start; } /* right-side controls share one width so the column reads as one rail */ .setting-row > .btn-ghost, .setting-row .confirm-del { flex: none; } /* operator config (artifacts, jobs): every control sits in the same-width @@ -936,6 +939,10 @@ a.btn-ghost { text-decoration: none; } and only take the accent on hover. */ .kv a { color: inherit; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; } .kv a:hover { color: var(--accent); text-decoration-style: solid; } +/* A failure value: the timestamp is the row, and what went wrong hangs off the + (i) beside it. overflow is visible because .kv b clips for its ellipsis, and + that clipping would eat the tooltip card. */ +.kv b.kv-fail { display: inline-flex; align-items: center; gap: 6px; overflow: visible; color: var(--danger); } /* A kv used inside a setting's description sits tighter than a section-level one. */ .kv-inline { margin-top: 8px; } .kv-inline > div { padding: 6px 2px; font-size: 12.5px; }