Leandro von Werra Agent Manager Claude Opus 5 commited on
Commit
20d5668
·
unverified ·
1 Parent(s): 3798ea4

Settings: give the backup block room to read (#44)

Browse files

Three layout fixes to the backup setting, which had grown until the
interval control — the thing the block is actually about — was the
hardest part of it to find.

- The interval segments now sit level with the description instead of
centring against a column that runs a description, a kv table and a
thirty-chip tag field tall. Same rail position, no longer floating
halfway down the block.
- A failed run reports its timestamp and streak on one line; the Job's
message and reason move into the existing (i) tooltip. They were a
paragraph wrapping inside a right-aligned kv value, which pushed the
rest of the table around and made a table worth scanning into prose.
- The skip list, its help text and "Back up now" move out of the label
column to full width. Thirty chips wrapping in half the measure read
as a wall, and the field they belong to is a text input, which wants
the whole line.

No behaviour change: same controls, same handlers, same requests.

Co-authored-by: Agent Manager <agents@agent-manager.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

web/src/components/SettingsView.tsx CHANGED
@@ -403,7 +403,10 @@ export default function SettingsView({
403
  </span>
404
  </div>
405
 
406
- <div className="setting-row">
 
 
 
407
  <div>
408
  <div className="s-label">Back up the bucket</div>
409
  <div className="s-help">
@@ -447,18 +450,22 @@ export default function SettingsView({
447
  </div>
448
  {/* The dashboard strip says a backup is failing; this says
449
  what the Job actually reported, which is what tells you
450
- whether it is yours to fix. */}
 
 
 
451
  {bk.lastFailure && (
452
  <div>
453
  <span>Last failure</span>
454
- <b style={{ color: 'var(--danger)', whiteSpace: 'normal', textAlign: 'right' }}>
455
  {new Date(bk.lastFailure.at).toLocaleString()}
456
- {bk.lastFailure.message ? ` — ${bk.lastFailure.message}` : ''}
457
  {bk.failures > 1 ? ` (${bk.failures} in a row)` : ''}
458
- {bk.lastFailure.reason && (
459
- <div className="mono" style={{ fontWeight: 400, color: 'var(--muted)', fontSize: 11.5, marginTop: 2 }}>
460
- {bk.lastFailure.reason}
461
- </div>
 
 
462
  )}
463
  </b>
464
  </div>
@@ -471,80 +478,6 @@ export default function SettingsView({
471
  )}
472
  </div>
473
  )}
474
- {/* On demand regardless of the interval: taking one backup
475
- before a risky change should not mean switching on a
476
- schedule. Disabled while a run is in flight — two Jobs
477
- uploading to one dataset would race. */}
478
- {/* Folders to keep out of the history. An env directory is
479
- thousands of files the backup has to hash and none of them
480
- worth keeping — measured, that is 7s of work versus 1s. */}
481
- {bk?.canRunNow && (
482
- <>
483
- <div className="s-help skiphead" style={{ marginTop: 10 }}>
484
- <span>
485
- Skip these folders — type a name and press Enter. Anywhere they appear
486
- (<span className="mono">node_modules</span>, <span className="mono">.venv</span>),
487
- they stay out of the history. Everything here is something a command puts
488
- back; nothing is skipped for being large. Remove any you want kept.
489
- </span>
490
- {/* The default list is long and picked from measurements, so an operator
491
- who trims it has no way back without retyping 30 names. Hidden when
492
- the list already matches the defaults, so it is never a no-op —
493
- compared against the local edit rather than the server's copy, or it
494
- would linger until the next status poll. */}
495
- {bk.excludeDefaults?.length > 0
496
- && (cfg.backup.exclude.length !== bk.excludeDefaults.length
497
- || cfg.backup.exclude.some((t, i) => t !== bk.excludeDefaults[i])) && (
498
- <button
499
- className="btn-ghost skiprestore"
500
- onClick={() => setCfg({ ...cfg, backup: { ...cfg.backup, exclude: [...bk.excludeDefaults] } })}
501
- >Restore defaults</button>
502
- )}
503
- </div>
504
- <div className="tagf" onClick={(e) => {
505
- if (e.target === e.currentTarget) (e.currentTarget.querySelector('input') as HTMLInputElement)?.focus();
506
- }}>
507
- {cfg.backup.exclude.map((t) => (
508
- <span className="tagf-chip mono" key={t}>
509
- {t}
510
- <button
511
- className="tagf-x"
512
- aria-label={`Stop skipping ${t}`}
513
- onClick={() => setCfg({ ...cfg, backup: { ...cfg.backup, exclude: cfg.backup.exclude.filter((x) => x !== t) } })}
514
- >×</button>
515
- </span>
516
- ))}
517
- <input
518
- className="tagf-input mono"
519
- placeholder={cfg.backup.exclude.length ? 'add another…' : 'node_modules'}
520
- value={skipDraft}
521
- onChange={(e) => setSkipDraft(e.target.value)}
522
- onKeyDown={(e) => {
523
- // Enter or comma commits; Backspace on an empty box
524
- // takes the last chip back, as tag fields do.
525
- if (e.key === 'Enter' || e.key === ',') {
526
- e.preventDefault();
527
- addSkip();
528
- } else if (e.key === 'Backspace' && !skipDraft && cfg.backup.exclude.length) {
529
- setCfg({ ...cfg, backup: { ...cfg.backup, exclude: cfg.backup.exclude.slice(0, -1) } });
530
- }
531
- }}
532
- onBlur={addSkip}
533
- />
534
- </div>
535
- </>
536
- )}
537
- {bk?.canRunNow && (
538
- <button
539
- className="btn-ghost"
540
- style={{ marginTop: 8 }}
541
- disabled={bkBusy || bk.running}
542
- onClick={doBackup}
543
- >
544
- {bkBusy ? 'Launching…' : bk.running ? 'Backing up…' : 'Back up now'}
545
- </button>
546
- )}
547
- {bkMsg && <div className="s-help" style={{ marginTop: 6 }}>{bkMsg}</div>}
548
  </div>
549
  <span className="cfg-ctl">
550
  <div className="seg cfg-seg">
@@ -578,6 +511,84 @@ export default function SettingsView({
578
  </div>
579
  )}
580
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  <div className="setting-row">
582
  <div>
583
  <div className="s-label">Archive inactive sessions</div>
 
403
  </span>
404
  </div>
405
 
406
+ {/* row-top: the description runs four lines, and the interval is
407
+ the thing you read it for — it belongs level with the text, not
408
+ floating halfway down beside it. */}
409
+ <div className="setting-row row-top">
410
  <div>
411
  <div className="s-label">Back up the bucket</div>
412
  <div className="s-help">
 
450
  </div>
451
  {/* The dashboard strip says a backup is failing; this says
452
  what the Job actually reported, which is what tells you
453
+ whether it is yours to fix. The message and the Job's own
454
+ reason are a paragraph you read once, so they hang off the
455
+ timestamp as a tooltip rather than wrapping across the
456
+ table and pushing every other row around. */}
457
  {bk.lastFailure && (
458
  <div>
459
  <span>Last failure</span>
460
+ <b className="kv-fail">
461
  {new Date(bk.lastFailure.at).toLocaleString()}
 
462
  {bk.failures > 1 ? ` (${bk.failures} in a row)` : ''}
463
+ {(bk.lastFailure.message || bk.lastFailure.reason) && (
464
+ <span
465
+ className="tip"
466
+ tabIndex={0}
467
+ data-tip={[bk.lastFailure.message, bk.lastFailure.reason].filter(Boolean).join(' — ')}
468
+ ><InfoGlyph className="tip-i" /></span>
469
  )}
470
  </b>
471
  </div>
 
478
  )}
479
  </div>
480
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  </div>
482
  <span className="cfg-ctl">
483
  <div className="seg cfg-seg">
 
511
  </div>
512
  )}
513
 
514
+ {/* Folders to keep out of the history. An env directory is
515
+ thousands of files the backup has to hash and none of them
516
+ worth keeping — measured, that is 7s of work versus 1s.
517
+ Full width, outside the row: thirty chips wrapping inside the
518
+ label column read as a wall, and the field they belong to is a
519
+ text input, which wants the whole measure. */}
520
+ {bk?.canRunNow && (
521
+ <>
522
+ <div className="s-help skiphead" style={{ marginTop: 10 }}>
523
+ <span>
524
+ Skip these folders — type a name and press Enter. Anywhere they appear
525
+ (<span className="mono">node_modules</span>, <span className="mono">.venv</span>),
526
+ they stay out of the history. Everything here is something a command puts
527
+ back; nothing is skipped for being large. Remove any you want kept.
528
+ </span>
529
+ {/* The default list is long and picked from measurements, so an operator
530
+ who trims it has no way back without retyping 30 names. Hidden when
531
+ the list already matches the defaults, so it is never a no-op —
532
+ compared against the local edit rather than the server's copy, or it
533
+ would linger until the next status poll. */}
534
+ {bk.excludeDefaults?.length > 0
535
+ && (cfg.backup.exclude.length !== bk.excludeDefaults.length
536
+ || cfg.backup.exclude.some((t, i) => t !== bk.excludeDefaults[i])) && (
537
+ <button
538
+ className="btn-ghost skiprestore"
539
+ onClick={() => setCfg({ ...cfg, backup: { ...cfg.backup, exclude: [...bk.excludeDefaults] } })}
540
+ >Restore defaults</button>
541
+ )}
542
+ </div>
543
+ <div className="tagf" onClick={(e) => {
544
+ if (e.target === e.currentTarget) (e.currentTarget.querySelector('input') as HTMLInputElement)?.focus();
545
+ }}>
546
+ {cfg.backup.exclude.map((t) => (
547
+ <span className="tagf-chip mono" key={t}>
548
+ {t}
549
+ <button
550
+ className="tagf-x"
551
+ aria-label={`Stop skipping ${t}`}
552
+ onClick={() => setCfg({ ...cfg, backup: { ...cfg.backup, exclude: cfg.backup.exclude.filter((x) => x !== t) } })}
553
+ >×</button>
554
+ </span>
555
+ ))}
556
+ <input
557
+ className="tagf-input mono"
558
+ placeholder={cfg.backup.exclude.length ? 'add another…' : 'node_modules'}
559
+ value={skipDraft}
560
+ onChange={(e) => setSkipDraft(e.target.value)}
561
+ onKeyDown={(e) => {
562
+ // Enter or comma commits; Backspace on an empty box
563
+ // takes the last chip back, as tag fields do.
564
+ if (e.key === 'Enter' || e.key === ',') {
565
+ e.preventDefault();
566
+ addSkip();
567
+ } else if (e.key === 'Backspace' && !skipDraft && cfg.backup.exclude.length) {
568
+ setCfg({ ...cfg, backup: { ...cfg.backup, exclude: cfg.backup.exclude.slice(0, -1) } });
569
+ }
570
+ }}
571
+ onBlur={addSkip}
572
+ />
573
+ </div>
574
+ </>
575
+ )}
576
+ {/* On demand regardless of the interval: taking one backup
577
+ before a risky change should not mean switching on a
578
+ schedule. Disabled while a run is in flight — two Jobs
579
+ uploading to one dataset would race. */}
580
+ {bk?.canRunNow && (
581
+ <button
582
+ className="btn-ghost"
583
+ style={{ marginTop: 8 }}
584
+ disabled={bkBusy || bk.running}
585
+ onClick={doBackup}
586
+ >
587
+ {bkBusy ? 'Launching…' : bk.running ? 'Backing up…' : 'Back up now'}
588
+ </button>
589
+ )}
590
+ {bkMsg && <div className="s-help" style={{ marginTop: 6 }}>{bkMsg}</div>}
591
+
592
  <div className="setting-row">
593
  <div>
594
  <div className="s-label">Archive inactive sessions</div>
web/src/styles.css CHANGED
@@ -871,6 +871,9 @@ body {
871
  .settings-page h3 { margin: 24px 0 8px; font-size: 14px; }
872
  /* plain rows, not cards: the bold label is the section header */
873
  .setting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 2px; }
 
 
 
874
  /* right-side controls share one width so the column reads as one rail */
875
  .setting-row > .btn-ghost, .setting-row .confirm-del { flex: none; }
876
  /* operator config (artifacts, jobs): every control sits in the same-width
@@ -936,6 +939,10 @@ a.btn-ghost { text-decoration: none; }
936
  and only take the accent on hover. */
937
  .kv a { color: inherit; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
938
  .kv a:hover { color: var(--accent); text-decoration-style: solid; }
 
 
 
 
939
  /* A kv used inside a setting's description sits tighter than a section-level one. */
940
  .kv-inline { margin-top: 8px; }
941
  .kv-inline > div { padding: 6px 2px; font-size: 12.5px; }
 
871
  .settings-page h3 { margin: 24px 0 8px; font-size: 14px; }
872
  /* plain rows, not cards: the bold label is the section header */
873
  .setting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 2px; }
874
+ /* a row whose description runs several lines: the control sits level with the
875
+ text instead of centring against it, so the eye finds it where it starts */
876
+ .setting-row.row-top { align-items: flex-start; }
877
  /* right-side controls share one width so the column reads as one rail */
878
  .setting-row > .btn-ghost, .setting-row .confirm-del { flex: none; }
879
  /* operator config (artifacts, jobs): every control sits in the same-width
 
939
  and only take the accent on hover. */
940
  .kv a { color: inherit; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
941
  .kv a:hover { color: var(--accent); text-decoration-style: solid; }
942
+ /* A failure value: the timestamp is the row, and what went wrong hangs off the
943
+ (i) beside it. overflow is visible because .kv b clips for its ellipsis, and
944
+ that clipping would eat the tooltip card. */
945
+ .kv b.kv-fail { display: inline-flex; align-items: center; gap: 6px; overflow: visible; color: var(--danger); }
946
  /* A kv used inside a setting's description sits tighter than a section-level one. */
947
  .kv-inline { margin-top: 8px; }
948
  .kv-inline > div { padding: 6px 2px; font-size: 12.5px; }