Substantial feature + reliability sweep. Each version below was developed,
tested live against the maple/TrueNAS deployment, and Codex-reviewed
before bundling.
1.0.0-13 — asyncssh proc.kill() doesn't actually kill the remote process
(sshd ignores SSH signal-channel requests by default), so a cancel of a
long-running badblocks left the remote process running and proc.wait()
hanging — pinning the asyncio.Semaphore slot forever.
* Wrap long-lived commands in `sh -c 'echo PID:$$; exec <cmd>'` to
capture the remote PID; store in burnin._remote_pids[job_id].
* burnin._kill_remote_process(job_id) opens a fresh SSH session and
issues `kill -9 <pid>` — sshd honours that.
* Bound proc.wait() with asyncio.wait_for(timeout=15).
* burnin._active_tasks tracks every _run_job task so cancel_job and
check_stuck_jobs can actually cancel the asyncio task (was DB-only
before). Also fixes the documented asyncio.create_task GC gotcha
(weak refs only).
* _run_job finalizer reads current state and skips the write if state
!= 'running' so cancelled/unknown aren't clobbered.
1.0.0-14 — poller._upsert_drive ON CONFLICT only refreshed temperature/
health/poll timestamps; devname/serial/model/size_bytes were stuck at
first-INSERT values forever. After kernel SCSI re-enumeration two
drives could both show as `sda`. Fixed by updating all six fields.
Also added 7-day stale filter to _DRIVES_QUERY so removed drives drop
off the dashboard while audit/burnin_jobs FKs stay intact.
1.0.0-15/-16 — pool-membership lock.
* ssh_client.get_pool_membership() runs `zpool list -vHP` and parses
the flattened TrueNAS output (container vdevs + their device children
both appear at depth 1; section markers cache/log/spare/special/dedup
switch the role).
* ssh_client.get_zfs_member_drives() runs `lsblk -no NAME,FSTYPE -l`
to detect drives carrying ZFS labels not in any active pool — they
get pool_name='(exported)', pool_role='exported'.
* Three idempotent ALTER TABLE migrations on drives:
pool_name/pool_role/pool_seen_at.
* burnin.start_job raises PoolMemberError if pool_name IS NOT NULL and
the drive isn't in burnin._unlock_grants. Routes layer maps to 409
with structured detail {pool_name, pool_role, pool_locked: true} so
the frontend can render an unlock affordance.
* POST /api/v1/drives/{id}/unlock accepts {confirm_token, operator,
reason}. Token is the pool name for active pools, "DESTROY BOOT POOL"
for boot-pool, "DESTROY EXPORTED POOL" for exported. Reason >= 5
chars. TTL = UNLOCK_TTL_SECONDS = 600. Audit event types:
pool_drive_unlocked / boot_pool_drive_unlocked /
exported_pool_drive_unlocked.
* Grants are in-memory only — container restart wipes them.
* UI: lock icon (yellow/red/orange), pool pill, conditional Unlock vs
Burn-In button. modal_unlock.html with type-to-confirm field.
Live unlock countdown via tickUnlockCountdowns() in app.js.
* Daily report: red banner listing every unlock event from the last
24h, with operator + reason + timestamp.
1.0.0-17 — Codex review fail-open + XSS + structured-error fixes.
* ssh_client.get_pool_membership / get_zfs_member_drives now return
None on failure (vs {} for 'definitely empty'). poller passes
update_pool=False to _upsert_drive on detection failure, preserving
existing pool columns instead of clearing them. Without this fix a
1-second SSH blip silently unlocked every drive.
* mailer._build_unlock_banner_html escapes every interpolated field
via html.escape() (was '<' only). Time filter switched to
julianday() — string >= against datetime('now', '-1 day') compared
formats with different separators ('T' vs ' ') and timezone
suffixes, causing subtle off-by-N-hour inclusion.
* app.js submitStart/submitBatchStart now detect the structured
pool_locked 409 detail and auto-open the unlock modal for the
offending drive (was [object Object] in toast).
1.0.0-18 — Codex grant-binding + commit-ordering fixes.
* Unlock grants bound to the (pool_name, pool_role) observed at unlock
time. _UnlockGrant dataclass; _is_unlocked and unlock_expiry
invalidate the grant if the live row's pool identity has changed.
Prevents an 'exported' unlock from carrying over when the drive
turns out to be in active 'tank' or 'boot-pool'.
* grant_pool_unlock now writes to _unlock_grants only AFTER db.commit()
succeeds — previously a failed audit insert left an unaudited grant
armed.
1.0.0-19 — Codex race + cancellation classification + test scaffold.
* Partial unique index uniq_active_burnin_per_drive ON burnin_jobs
(drive_id) WHERE state IN ('queued','running'). INSERT now wraps in
try/except aiosqlite.IntegrityError -> ValueError so the read-then-
insert race in start_job can't produce two queued rows for the same
drive.
* _run_job tracks was_cancelled flag; on bare task.cancel() (shutdown,
future code paths) where DB state is still 'running', finalizer
writes 'unknown' instead of mis-classifying as 'failed'.
* tests/ stdlib unittest scaffold:
- test_pool_parser.py (21 tests): mirror/raidz/draid container vdevs,
single-disk depth-1, plural section markers, partition stripping,
sdaa-style names, multi-pool, role reset between pools.
- test_unlock_flow.py (18 tests): token validation per pool kind,
identity-binding invalidation, TTL expiry, audit-commit-then-arm
ordering, unique-active-burnin partial index.
Run via `python -m unittest discover tests/`. No new dependencies.
1.0.0-20 — Spearfoot-inspired badblocks tunables.
* surface_validate_block_size (-b, default 4096), surface_validate_
block_buffer (-c, default 64), surface_validate_passes (-p, default
1) exposed in Settings UI; persist via settings_store.json.
Validation: block size must be a power of 2 between 512 and
1048576. Defaults preserve existing behaviour. Bumping to 8192/64/1
roughly halves runtime on multi-TB HDDs at ~2x RAM cost.
1.0.0-21 — SMART overall-health column actually populated.
* /api/v2.0/disk doesn't expose smart_health, so every drive defaulted
to UNKNOWN forever (only burn-in stages ever wrote a real value).
* ssh_client.get_smart_health_map([devnames]) runs `smartctl -H` for
all drives in a single SSH session, deterministically delimited with
@@devname@@ ... @@END@@ markers. Returns {devname: PASSED|FAILED|
UNKNOWN} or None on SSH failure.
* poller calls it every 5th cycle (~1 min at default 12s interval),
caches in _state['smart_health_cache'] so transient failures preserve
the previous values.
* Dashboard CSS: col-smart min-width 150 -> 95, horizontal padding 14
-> 6 so Short/Long SMART columns fit comfortably on a 13-inch
display.
* 5 additional parser tests (44 total, all passing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2544 lines
54 KiB
CSS
2544 lines
54 KiB
CSS
/* -----------------------------------------------------------------------
|
|
Variables
|
|
----------------------------------------------------------------------- */
|
|
:root {
|
|
--bg: #0d1117;
|
|
--bg-card: #161b22;
|
|
--bg-row-alt: #0f1319;
|
|
--border: #30363d;
|
|
--text: #c9d1d9;
|
|
--text-muted: #8b949e;
|
|
--text-strong: #f0f6fc;
|
|
|
|
--green: #3fb950;
|
|
--green-bg: rgba(63, 185, 80, 0.12);
|
|
--green-bd: rgba(63, 185, 80, 0.35);
|
|
|
|
--yellow: #d29922;
|
|
--yellow-bg: rgba(210, 153, 34, 0.12);
|
|
--yellow-bd: rgba(210, 153, 34, 0.35);
|
|
|
|
--red: #f85149;
|
|
--red-bg: rgba(248, 81, 73, 0.12);
|
|
--red-bd: rgba(248, 81, 73, 0.35);
|
|
|
|
--blue: #58a6ff;
|
|
--blue-bg: rgba(88, 166, 255, 0.12);
|
|
--blue-bd: rgba(88, 166, 255, 0.35);
|
|
|
|
--gray: #6e7681;
|
|
--gray-bg: rgba(110, 118, 129, 0.12);
|
|
--gray-bd: rgba(110, 118, 129, 0.35);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Reset
|
|
----------------------------------------------------------------------- */
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
a { color: var(--blue); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Header
|
|
----------------------------------------------------------------------- */
|
|
header {
|
|
background: var(--bg-card);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 10px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.header-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: var(--text-strong);
|
|
}
|
|
|
|
.header-brand svg {
|
|
color: var(--blue);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.header-title {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.header-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.live-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.live-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--green);
|
|
box-shadow: 0 0 0 2px var(--green-bg);
|
|
animation: pulse-dot 2.5s ease-in-out infinite;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.live-dot.degraded {
|
|
background: var(--red);
|
|
box-shadow: 0 0 0 2px var(--red-bg);
|
|
animation: none;
|
|
}
|
|
|
|
@keyframes pulse-dot {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.35; }
|
|
}
|
|
|
|
.poll-time {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.header-link {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 2px 8px;
|
|
}
|
|
|
|
.header-link:hover {
|
|
color: var(--text);
|
|
border-color: var(--text-muted);
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Banners
|
|
----------------------------------------------------------------------- */
|
|
.banner {
|
|
padding: 8px 24px;
|
|
font-size: 13px;
|
|
border-bottom: 1px solid transparent;
|
|
}
|
|
|
|
.banner-warn {
|
|
background: var(--yellow-bg);
|
|
border-color: var(--yellow-bd);
|
|
color: var(--yellow);
|
|
}
|
|
|
|
.banner-error {
|
|
background: var(--red-bg);
|
|
border-color: var(--red-bd);
|
|
color: var(--red);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Main layout
|
|
----------------------------------------------------------------------- */
|
|
main {
|
|
padding: 20px 24px 40px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Filter bar
|
|
----------------------------------------------------------------------- */
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-bottom: 14px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
color: var(--text-muted);
|
|
border-radius: 6px;
|
|
padding: 5px 12px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
|
}
|
|
|
|
.filter-btn:hover {
|
|
color: var(--text);
|
|
border-color: #58a6ff55;
|
|
}
|
|
|
|
.filter-btn.active {
|
|
color: var(--blue);
|
|
background: var(--blue-bg);
|
|
border-color: var(--blue-bd);
|
|
}
|
|
|
|
.filter-btn .badge {
|
|
background: var(--bg);
|
|
border-radius: 10px;
|
|
padding: 0 6px;
|
|
font-size: 11px;
|
|
font-variant-numeric: tabular-nums;
|
|
min-width: 20px;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.filter-btn.active .badge {
|
|
background: var(--blue-bg);
|
|
color: var(--blue);
|
|
border-color: var(--blue-bd);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Table wrapper
|
|
overflow: auto on BOTH axes is required for position:sticky on thead
|
|
to work correctly. overflow-x:auto alone creates a stacking context
|
|
that causes tbody to render behind the sticky header.
|
|
----------------------------------------------------------------------- */
|
|
.table-wrap {
|
|
overflow: auto;
|
|
max-height: calc(100vh - 205px); /* header(44) + main-pad(20) + stats-bar(70) + filter-bar(46) + buffer */
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
thead {
|
|
background: var(--bg-card);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
th {
|
|
padding: 9px 14px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--text-muted);
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
td {
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
tr:nth-child(even) td {
|
|
background: var(--bg-row-alt);
|
|
}
|
|
|
|
tr:hover td {
|
|
background: rgba(88, 166, 255, 0.04);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Column widths
|
|
----------------------------------------------------------------------- */
|
|
.col-drive { min-width: 180px; }
|
|
.col-serial { min-width: 110px; }
|
|
.col-size { min-width: 70px; text-align: right; }
|
|
.col-temp { min-width: 75px; text-align: right; }
|
|
.col-health { min-width: 85px; }
|
|
.col-smart { min-width: 95px; }
|
|
/* Tighter horizontal padding on the SMART columns — they hold short
|
|
pills ("Passed"/"—") or a progress bar, so the default 14px gutter
|
|
wastes space on 13" laptops. */
|
|
th.col-smart, td.col-smart { padding-left: 6px; padding-right: 6px; }
|
|
.col-actions { min-width: 170px; }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Drive cell
|
|
----------------------------------------------------------------------- */
|
|
.drive-name {
|
|
display: block;
|
|
font-weight: 500;
|
|
color: var(--text-strong);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.drive-model {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 1px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Misc cell types
|
|
----------------------------------------------------------------------- */
|
|
.mono {
|
|
font-family: "SF Mono", "Cascadia Code", "Fira Mono", "Consolas", monospace;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.cell-empty {
|
|
color: var(--border);
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Temperature
|
|
----------------------------------------------------------------------- */
|
|
.temp {
|
|
font-weight: 500;
|
|
font-size: 13px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.temp-cool { color: var(--green); }
|
|
.temp-warm { color: var(--yellow); }
|
|
.temp-hot { color: var(--red); }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Status chips
|
|
----------------------------------------------------------------------- */
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
border-radius: 4px;
|
|
padding: 2px 8px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
border: 1px solid transparent;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.chip-passed {
|
|
color: var(--green);
|
|
background: var(--green-bg);
|
|
border-color: var(--green-bd);
|
|
}
|
|
|
|
.chip-failed {
|
|
color: var(--red);
|
|
background: var(--red-bg);
|
|
border-color: var(--red-bd);
|
|
cursor: help;
|
|
}
|
|
|
|
.chip-running {
|
|
color: var(--blue);
|
|
background: var(--blue-bg);
|
|
border-color: var(--blue-bd);
|
|
}
|
|
|
|
.chip-aborted {
|
|
color: var(--yellow);
|
|
background: var(--yellow-bg);
|
|
border-color: var(--yellow-bd);
|
|
}
|
|
|
|
.chip-unknown {
|
|
color: var(--gray);
|
|
background: var(--gray-bg);
|
|
border-color: var(--gray-bd);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
SMART cell — progress + ETA
|
|
----------------------------------------------------------------------- */
|
|
.smart-cell {
|
|
min-width: 140px;
|
|
}
|
|
|
|
.progress-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.progress-bar {
|
|
flex: 1;
|
|
height: 5px;
|
|
background: var(--border);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
min-width: 80px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: var(--blue);
|
|
border-radius: 3px;
|
|
transition: width 0.6s ease;
|
|
}
|
|
|
|
.progress-pct {
|
|
font-size: 12px;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-muted);
|
|
min-width: 32px;
|
|
text-align: right;
|
|
}
|
|
|
|
.eta-text {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Burn-in column
|
|
----------------------------------------------------------------------- */
|
|
.col-burnin { min-width: 160px; }
|
|
|
|
.burnin-cell { min-width: 140px; }
|
|
|
|
.stage-name {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 3px;
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
.chip-queued {
|
|
color: var(--yellow);
|
|
background: var(--yellow-bg);
|
|
border-color: var(--yellow-bd);
|
|
}
|
|
|
|
.progress-fill-green {
|
|
background: var(--green);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Action buttons
|
|
----------------------------------------------------------------------- */
|
|
.action-group {
|
|
display: flex;
|
|
gap: 5px;
|
|
flex-wrap: nowrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn-action {
|
|
border-radius: 5px;
|
|
padding: 4px 9px;
|
|
font-size: 11px;
|
|
font-family: inherit;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
border: 1px solid transparent;
|
|
transition: opacity 0.12s;
|
|
white-space: nowrap;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.btn-action:hover:not(:disabled) { opacity: 0.8; }
|
|
.btn-action:disabled,
|
|
.btn-disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-smart-short {
|
|
background: var(--blue-bg);
|
|
color: var(--blue);
|
|
border-color: var(--blue-bd);
|
|
}
|
|
|
|
.btn-smart-long {
|
|
background: var(--yellow-bg);
|
|
color: var(--yellow);
|
|
border-color: var(--yellow-bd);
|
|
}
|
|
|
|
.btn-start {
|
|
background: var(--red-bg);
|
|
color: var(--red);
|
|
border-color: var(--red-bd);
|
|
}
|
|
|
|
.btn-cancel {
|
|
background: var(--red-bg);
|
|
color: var(--red);
|
|
border-color: var(--red-bd);
|
|
}
|
|
|
|
.btn-cancel-smart {
|
|
background: var(--yellow-bg);
|
|
color: var(--yellow);
|
|
border-color: var(--yellow-bd);
|
|
}
|
|
|
|
/* Cancel All Running Burn-Ins button in the filter bar */
|
|
.btn-cancel-all {
|
|
margin-left: auto;
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
border: 1px solid var(--red-bd);
|
|
border-radius: 6px;
|
|
background: var(--red-bg);
|
|
color: var(--red);
|
|
cursor: pointer;
|
|
transition: background .15s, border-color .15s;
|
|
white-space: nowrap;
|
|
}
|
|
.btn-cancel-all:hover {
|
|
background: rgba(248, 81, 73, 0.22);
|
|
border-color: var(--red);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Modal overlay + dialog
|
|
----------------------------------------------------------------------- */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.65);
|
|
z-index: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.modal-overlay[hidden] { display: none; }
|
|
|
|
.modal {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
width: 100%;
|
|
max-width: 480px;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: var(--text-strong);
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-muted);
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
line-height: 1;
|
|
}
|
|
.modal-close:hover { background: var(--border); color: var(--text); }
|
|
|
|
.modal-body {
|
|
padding: 18px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
padding: 14px 20px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* Drive info block */
|
|
.modal-drive-info {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 12px 14px;
|
|
}
|
|
|
|
.modal-drive-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.modal-devname {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: var(--text-strong);
|
|
}
|
|
|
|
.modal-drive-sub {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Form elements */
|
|
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
|
|
|
.form-label {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.form-input {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
padding: 8px 12px;
|
|
outline: none;
|
|
transition: border-color 0.12s;
|
|
}
|
|
.form-input:focus { border-color: var(--blue); }
|
|
|
|
.form-input-confirm {
|
|
font-family: "SF Mono", "Cascadia Code", monospace;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Profile cards */
|
|
.profile-options {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.profile-option {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.profile-option input[type="radio"] {
|
|
position: absolute;
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.profile-card {
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 10px 12px;
|
|
transition: border-color 0.12s, background 0.12s;
|
|
}
|
|
|
|
.profile-option input:checked + .profile-card {
|
|
border-color: var(--blue);
|
|
background: var(--blue-bg);
|
|
}
|
|
|
|
.profile-card-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-strong);
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.profile-card-desc {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Confirmation warning */
|
|
.confirm-warning {
|
|
background: var(--red-bg);
|
|
border: 1px solid var(--red-bd);
|
|
border-radius: 6px;
|
|
color: var(--red);
|
|
font-size: 13px;
|
|
padding: 10px 12px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.confirm-hint {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Modal buttons */
|
|
.btn-primary {
|
|
background: var(--blue);
|
|
color: #fff;
|
|
border: 1px solid var(--blue);
|
|
border-radius: 6px;
|
|
padding: 7px 16px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: opacity 0.12s;
|
|
}
|
|
.btn-primary:hover:not(:disabled) { opacity: 0.85; }
|
|
.btn-primary:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
|
|
.btn-secondary {
|
|
background: none;
|
|
color: var(--text-muted);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 7px 16px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.btn-secondary:hover { color: var(--text); border-color: var(--text-muted); }
|
|
|
|
.btn-danger {
|
|
background: var(--red);
|
|
color: #fff;
|
|
border: 1px solid var(--red);
|
|
border-radius: 6px;
|
|
padding: 7px 16px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: opacity 0.12s;
|
|
}
|
|
.btn-danger:hover:not(:disabled) { opacity: 0.85; }
|
|
.btn-danger:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Toast notifications
|
|
----------------------------------------------------------------------- */
|
|
#toast-container {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
z-index: 600;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
pointer-events: none;
|
|
transition: bottom 0.25s ease;
|
|
}
|
|
|
|
body.drawer-open #toast-container {
|
|
bottom: calc(45vh + 16px);
|
|
}
|
|
|
|
.toast {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px 16px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
animation: toast-in 0.2s ease;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.toast-success { border-left: 3px solid var(--green); color: var(--green); }
|
|
.toast-error { border-left: 3px solid var(--red); color: var(--red); }
|
|
.toast-info { border-left: 3px solid var(--blue); color: var(--blue); }
|
|
|
|
@keyframes toast-in {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Empty state
|
|
----------------------------------------------------------------------- */
|
|
.empty-state {
|
|
padding: 48px;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
History / detail page chrome
|
|
----------------------------------------------------------------------- */
|
|
.page-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--text-strong);
|
|
}
|
|
|
|
.toolbar-right {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn-export {
|
|
display: inline-block;
|
|
background: var(--green-bg);
|
|
color: var(--green);
|
|
border: 1px solid var(--green-bd);
|
|
border-radius: 6px;
|
|
padding: 5px 14px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
transition: opacity 0.12s;
|
|
}
|
|
.btn-export:hover { opacity: 0.8; text-decoration: none; }
|
|
|
|
.btn-detail {
|
|
display: inline-block;
|
|
background: var(--bg-card);
|
|
color: var(--text-muted);
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
padding: 3px 10px;
|
|
font-size: 12px;
|
|
text-decoration: none;
|
|
transition: color 0.12s, border-color 0.12s;
|
|
}
|
|
.btn-detail:hover { color: var(--text); border-color: var(--text-muted); text-decoration: none; }
|
|
|
|
.text-muted { color: var(--text-muted); }
|
|
|
|
.col-job { min-width: 48px; }
|
|
|
|
.error-cell { max-width: 260px; }
|
|
.error-snippet {
|
|
color: var(--red);
|
|
font-size: 12px;
|
|
cursor: help;
|
|
display: block;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 240px;
|
|
}
|
|
|
|
.error-full {
|
|
color: var(--red);
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Pagination
|
|
----------------------------------------------------------------------- */
|
|
.pagination {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-top: 14px;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.page-btn {
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
text-decoration: none;
|
|
transition: color 0.12s, border-color 0.12s;
|
|
}
|
|
.page-btn:hover { color: var(--text); border-color: var(--text-muted); text-decoration: none; }
|
|
|
|
.page-info { color: var(--text-muted); font-size: 12px; }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Breadcrumb
|
|
----------------------------------------------------------------------- */
|
|
.breadcrumb {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.breadcrumb a { color: var(--blue); }
|
|
.breadcrumb-sep { color: var(--border); }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Detail page — summary grid
|
|
----------------------------------------------------------------------- */
|
|
.detail-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 14px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
@media (max-width: 680px) {
|
|
.detail-grid { grid-template-columns: 1fr; }
|
|
}
|
|
|
|
.detail-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.detail-card-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--text-muted);
|
|
padding: 10px 14px 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.detail-rows { padding: 4px 0; }
|
|
|
|
.detail-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 7px 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.detail-row:last-child { border-bottom: none; }
|
|
|
|
.detail-label {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
flex-shrink: 0;
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.detail-value {
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
text-align: right;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.stage-label {
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* chip-cancelled reuses gray */
|
|
.chip-cancelled {
|
|
color: var(--gray);
|
|
background: var(--gray-bg);
|
|
border-color: var(--gray-bd);
|
|
}
|
|
|
|
/* chip-red for full profile label */
|
|
.chip-red {
|
|
color: var(--red);
|
|
background: var(--red-bg);
|
|
border-color: var(--red-bd);
|
|
}
|
|
|
|
/* chip-gray for quick profile label */
|
|
.chip-gray {
|
|
color: var(--gray);
|
|
background: var(--gray-bg);
|
|
border-color: var(--gray-bd);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Notification bell button
|
|
----------------------------------------------------------------------- */
|
|
.notif-btn {
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 26px;
|
|
height: 26px;
|
|
padding: 0;
|
|
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
|
}
|
|
|
|
.notif-btn:hover {
|
|
color: var(--text);
|
|
border-color: var(--text-muted);
|
|
}
|
|
|
|
.notif-btn.notif-active {
|
|
color: var(--green);
|
|
background: var(--green-bg);
|
|
border-color: var(--green-bd);
|
|
}
|
|
|
|
.notif-btn.notif-denied {
|
|
opacity: 0.35;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Stats bar — top of dashboard
|
|
----------------------------------------------------------------------- */
|
|
.stats-bar {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 14px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px 18px;
|
|
text-align: center;
|
|
min-width: 80px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
text-decoration: none;
|
|
transition: border-color 0.12s;
|
|
}
|
|
|
|
a.stat-card:hover {
|
|
border-color: var(--text-muted);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
color: var(--text-strong);
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.stat-running .stat-value { color: var(--blue); }
|
|
.stat-failed .stat-value { color: var(--red); }
|
|
.stat-passed .stat-value { color: var(--green); }
|
|
.stat-idle .stat-value { color: var(--text-muted); }
|
|
|
|
/* Vertical separator between drive-count cards and sensor chips */
|
|
.stats-bar-sep {
|
|
width: 1px;
|
|
height: 36px;
|
|
background: var(--border);
|
|
align-self: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Compact sensor chip — CPU / PCH / Thermal */
|
|
.stat-sensor {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 6px 12px;
|
|
text-align: center;
|
|
min-width: 52px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.stat-sensor-val {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.stat-sensor-label {
|
|
font-size: 9px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--text-muted);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
/* Thermal pressure states */
|
|
.stat-sensor-thermal-warn {
|
|
border-color: var(--yellow-bd);
|
|
background: var(--yellow-bg);
|
|
}
|
|
.stat-sensor-thermal-warn .stat-sensor-val { color: var(--yellow); }
|
|
|
|
.stat-sensor-thermal-crit {
|
|
border-color: var(--red-bd);
|
|
background: var(--red-bg);
|
|
}
|
|
.stat-sensor-thermal-crit .stat-sensor-val { color: var(--red); }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Batch action bar (inside filter-bar)
|
|
----------------------------------------------------------------------- */
|
|
.batch-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-left: auto;
|
|
background: var(--blue-bg);
|
|
border: 1px solid var(--blue-bd);
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
}
|
|
|
|
.batch-count-label {
|
|
font-size: 12px;
|
|
color: var(--blue);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.btn-batch-start {
|
|
background: var(--blue);
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
font-family: inherit;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: opacity 0.12s;
|
|
}
|
|
.btn-batch-start:hover { opacity: 0.85; }
|
|
|
|
.btn-batch-clear {
|
|
background: none;
|
|
color: var(--blue);
|
|
border: 1px solid var(--blue-bd);
|
|
border-radius: 5px;
|
|
padding: 3px 8px;
|
|
font-size: 11px;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.btn-batch-clear:hover { background: var(--blue-bg); }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Checkbox column
|
|
----------------------------------------------------------------------- */
|
|
.col-check {
|
|
width: 36px;
|
|
min-width: 36px;
|
|
padding: 10px 8px 10px 14px;
|
|
}
|
|
|
|
.drive-checkbox, #select-all-cb {
|
|
width: 15px;
|
|
height: 15px;
|
|
cursor: pointer;
|
|
accent-color: var(--blue);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Drive location inline edit
|
|
----------------------------------------------------------------------- */
|
|
.drive-location {
|
|
display: block;
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
margin-top: 2px;
|
|
cursor: pointer;
|
|
border-radius: 3px;
|
|
padding: 1px 3px;
|
|
transition: background 0.1s;
|
|
max-width: 160px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.drive-location:hover { background: var(--border); color: var(--text); }
|
|
|
|
.drive-location-empty {
|
|
color: var(--border);
|
|
font-style: italic;
|
|
}
|
|
.drive-location-empty:hover { color: var(--text-muted); background: var(--border); }
|
|
|
|
.drive-location-input {
|
|
display: block;
|
|
background: var(--bg);
|
|
border: 1px solid var(--blue);
|
|
border-radius: 3px;
|
|
color: var(--text);
|
|
font-size: 10px;
|
|
font-family: inherit;
|
|
padding: 1px 4px;
|
|
margin-top: 2px;
|
|
width: 140px;
|
|
outline: none;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Burn-in meta row (stage + elapsed)
|
|
----------------------------------------------------------------------- */
|
|
.burnin-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.elapsed-timer {
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Confirm checkbox (batch modal)
|
|
----------------------------------------------------------------------- */
|
|
.confirm-check-label {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
line-height: 1.5;
|
|
}
|
|
.confirm-check-label input[type="checkbox"] {
|
|
margin-top: 2px;
|
|
width: 15px;
|
|
height: 15px;
|
|
flex-shrink: 0;
|
|
accent-color: var(--red);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Settings page — two-column layout
|
|
----------------------------------------------------------------------- */
|
|
|
|
/* Outer two-column grid: SMTP left (wider), right col stacks */
|
|
.settings-two-col {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 12px;
|
|
align-items: start;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.settings-left-col,
|
|
.settings-right-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Card */
|
|
.settings-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
}
|
|
|
|
.settings-card-readonly {
|
|
opacity: .75;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.settings-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.settings-card-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: .06em;
|
|
color: var(--text-strong);
|
|
}
|
|
|
|
/* Compact 2-col grid for horizontal label|input layout in email card */
|
|
.sf-fields {
|
|
display: grid;
|
|
grid-template-columns: 84px 1fr;
|
|
align-items: center;
|
|
gap: 4px 8px;
|
|
}
|
|
.sf-fields > label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
/* Full-width rows that span both columns */
|
|
.sf-full { grid-column: 1 / -1; }
|
|
/* Inline sub-group (mode + timeout on one row) */
|
|
.sf-inline-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.sf-inline-group .sf-label-sm {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Field rows */
|
|
.sf-row {
|
|
margin-bottom: 8px;
|
|
}
|
|
.sf-row:last-child { margin-bottom: 0; }
|
|
|
|
.sf-label {
|
|
display: block;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
margin-bottom: 3px;
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
}
|
|
|
|
.sf-hint {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 2px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.sf-input {
|
|
width: 100%;
|
|
padding: 4px 8px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
outline: none;
|
|
font-family: inherit;
|
|
transition: border-color .15s;
|
|
}
|
|
.sf-input:focus { border-color: var(--blue); }
|
|
.sf-input.sf-input-xs { width: 80px; }
|
|
|
|
.sf-select {
|
|
padding: 4px 8px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
outline: none;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
transition: border-color .15s;
|
|
}
|
|
.sf-select:focus { border-color: var(--blue); }
|
|
|
|
/* Inline group of small fields */
|
|
.sf-row-inline {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
}
|
|
.sf-row-inline > div { display: flex; flex-direction: column; }
|
|
|
|
/* Test button inline row */
|
|
.sf-row-test {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* Toggle row (label + toggle side by side) */
|
|
.sf-toggle-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 7px 0;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.sf-toggle-row:first-of-type { border-top: none; padding-top: 0; }
|
|
.sf-toggle-row .sf-label { margin-bottom: 0; text-transform: none; font-size: 13px; color: var(--text); font-weight: 500; letter-spacing: 0; }
|
|
.sf-toggle-row .sf-hint { margin-top: 1px; }
|
|
|
|
/* Thin divider inside card */
|
|
.sf-divider {
|
|
height: 1px;
|
|
background: var(--border);
|
|
margin: 8px 0;
|
|
}
|
|
|
|
/* Read-only grid inside system card */
|
|
.sf-readonly-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0;
|
|
}
|
|
|
|
.sf-ro-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.sf-ro-row:nth-last-child(-n+2) { border-bottom: none; }
|
|
|
|
.sf-ro-label {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.sf-ro-value {
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
}
|
|
|
|
/* Save action bar */
|
|
.settings-save-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
@media (max-width: 860px) {
|
|
.settings-two-col {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.sf-readonly-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.sf-ro-row:nth-last-child(-n+2) { border-bottom: 1px solid var(--border); }
|
|
.sf-ro-row:last-child { border-bottom: none; }
|
|
}
|
|
|
|
.page-subtitle {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Stats / analytics page
|
|
----------------------------------------------------------------------- */
|
|
.stats-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.overview-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 14px 20px;
|
|
text-align: center;
|
|
min-width: 90px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.ov-value {
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
color: var(--text-strong);
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1;
|
|
}
|
|
|
|
.ov-label {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.ov-green .ov-value { color: var(--green); }
|
|
.ov-red .ov-value { color: var(--red); }
|
|
.ov-blue .ov-value { color: var(--blue); }
|
|
.ov-gray .ov-value { color: var(--text-muted); }
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
.stats-section {}
|
|
|
|
.rate-bar-wrap {
|
|
display: flex;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
background: var(--border);
|
|
min-width: 80px;
|
|
}
|
|
|
|
.rate-bar-fill { height: 100%; }
|
|
.rate-pass { background: var(--green); }
|
|
.rate-fail { background: var(--red); }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Mobile responsive
|
|
----------------------------------------------------------------------- */
|
|
@media (max-width: 900px) {
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.settings-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
header {
|
|
padding: 8px 14px;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.header-meta {
|
|
gap: 10px;
|
|
}
|
|
|
|
main {
|
|
padding: 12px 10px 32px;
|
|
}
|
|
|
|
/* Hide less critical table columns on small screens */
|
|
.col-size, .col-temp { display: none; }
|
|
|
|
/* Make drive name wrap */
|
|
.col-drive { min-width: 130px; }
|
|
.col-serial { min-width: 80px; }
|
|
|
|
.stats-bar {
|
|
gap: 6px;
|
|
}
|
|
|
|
.stat-card {
|
|
padding: 8px 12px;
|
|
min-width: 64px;
|
|
}
|
|
|
|
.stat-value { font-size: 18px; }
|
|
|
|
.detail-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.table-wrap {
|
|
max-height: calc(100vh - 240px);
|
|
}
|
|
|
|
.batch-bar {
|
|
margin-left: 0;
|
|
width: 100%;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.header-link { display: none; }
|
|
.notif-btn { display: none; }
|
|
.col-serial { display: none; }
|
|
.col-health { display: none; }
|
|
|
|
/* Settings page — iPhone */
|
|
.settings-card { padding: 10px 10px; }
|
|
.settings-save-bar { flex-wrap: wrap; gap: 8px; }
|
|
.settings-save-bar .btn-primary,
|
|
.settings-save-bar .btn-secondary { flex: 1 1 auto; text-align: center; }
|
|
.sf-row-inline { flex-direction: column; gap: 8px; }
|
|
.sf-input.sf-input-xs { width: 100%; }
|
|
.sf-fields { grid-template-columns: 72px 1fr; }
|
|
.page-subtitle { font-size: 11px; }
|
|
|
|
/* Filter bar — keep cancel-all from overflowing */
|
|
.btn-cancel-all { margin-left: 0; width: 100%; }
|
|
.filter-bar { flex-wrap: wrap; }
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Header brand — now an <a> tag, kill link styling
|
|
----------------------------------------------------------------------- */
|
|
a.header-brand {
|
|
text-decoration: none;
|
|
color: var(--text-strong);
|
|
}
|
|
a.header-brand:hover { text-decoration: none; }
|
|
a.header-brand:hover .header-title {
|
|
color: var(--blue);
|
|
transition: color .15s;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Buttons — secondary + primary variants
|
|
----------------------------------------------------------------------- */
|
|
.btn-primary {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 7px 18px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
background: var(--blue);
|
|
border: 1px solid var(--blue-bd);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: opacity .15s;
|
|
}
|
|
.btn-primary:hover { opacity: .85; }
|
|
.btn-primary:disabled { opacity: .45; cursor: default; }
|
|
|
|
.btn-secondary {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 14px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text);
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: border-color .15s, color .15s;
|
|
}
|
|
.btn-secondary:hover { border-color: var(--blue); color: var(--blue); }
|
|
.btn-secondary:disabled { opacity: .45; cursor: default; }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Settings form — sections and fields
|
|
----------------------------------------------------------------------- */
|
|
.page-subtitle {
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
margin: -4px 0 20px;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.page-subtitle code {
|
|
font-family: "SF Mono", "Cascadia Code", monospace;
|
|
font-size: 11px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 3px;
|
|
padding: 1px 5px;
|
|
color: var(--text);
|
|
}
|
|
|
|
.badge-restart {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: .04em;
|
|
color: var(--yellow);
|
|
background: var(--yellow-bg);
|
|
border: 1px solid var(--yellow-bd);
|
|
border-radius: 4px;
|
|
padding: 1px 7px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Section card */
|
|
.settings-section {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 20px 22px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.settings-section-readonly {
|
|
opacity: .8;
|
|
}
|
|
|
|
.settings-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.settings-section-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-strong);
|
|
text-transform: uppercase;
|
|
letter-spacing: .06em;
|
|
}
|
|
|
|
/* Individual field row */
|
|
.settings-field {
|
|
margin-bottom: 14px;
|
|
}
|
|
.settings-field:last-child { margin-bottom: 0; }
|
|
|
|
.settings-label {
|
|
display: block;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.settings-input {
|
|
width: 100%;
|
|
max-width: 520px;
|
|
padding: 7px 11px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
outline: none;
|
|
transition: border-color .15s;
|
|
font-family: inherit;
|
|
}
|
|
.settings-input:focus { border-color: var(--blue); }
|
|
.settings-input.settings-input-sm { max-width: 120px; }
|
|
|
|
.settings-select {
|
|
padding: 7px 11px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
outline: none;
|
|
cursor: pointer;
|
|
transition: border-color .15s;
|
|
font-family: inherit;
|
|
}
|
|
.settings-select:focus { border-color: var(--blue); }
|
|
|
|
.settings-hint {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Row with multiple fields side by side */
|
|
.settings-field-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
align-items: flex-end;
|
|
}
|
|
.settings-field-group { display: flex; flex-direction: column; }
|
|
.settings-field-group .settings-label { margin-bottom: 5px; }
|
|
|
|
/* Toggle row */
|
|
.settings-field-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
padding: 10px 0;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.settings-field-toggle:first-of-type { border-top: none; }
|
|
|
|
.settings-toggle-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.settings-toggle-info .settings-label { margin-bottom: 0; }
|
|
|
|
/* Thin divider inside section */
|
|
.settings-divider {
|
|
height: 1px;
|
|
background: var(--border);
|
|
margin: 14px 0;
|
|
}
|
|
|
|
/* Inline test/action row */
|
|
.settings-actions-inline {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin-top: 16px;
|
|
padding-top: 14px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* Save row */
|
|
.settings-save-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
/* Inline result text */
|
|
.settings-test-result {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
.result-ok { color: var(--green); }
|
|
.result-err { color: var(--red); }
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Stage selection (burn-in modals)
|
|
----------------------------------------------------------------------- */
|
|
.stage-checks {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.stage-check {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
cursor: pointer;
|
|
padding: 7px 10px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg);
|
|
transition: border-color .15s;
|
|
}
|
|
|
|
.stage-check:hover { border-color: var(--blue); }
|
|
|
|
.stage-check.dragging {
|
|
opacity: 0.4;
|
|
border-style: dashed;
|
|
}
|
|
|
|
.drag-handle {
|
|
color: var(--text-muted);
|
|
cursor: grab;
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
padding-top: 1px;
|
|
flex-shrink: 0;
|
|
user-select: none;
|
|
opacity: 0.5;
|
|
}
|
|
.drag-handle:active { cursor: grabbing; }
|
|
.stage-check:hover .drag-handle { opacity: 1; }
|
|
|
|
.stage-drag-hint {
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.stage-check input[type="checkbox"] {
|
|
margin-top: 2px;
|
|
flex-shrink: 0;
|
|
accent-color: var(--blue);
|
|
width: 15px;
|
|
height: 15px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.stage-check span { font-size: 13px; line-height: 1.4; }
|
|
|
|
.stage-tag {
|
|
display: inline-block;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
border-radius: 3px;
|
|
padding: 1px 5px;
|
|
margin-left: 5px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.stage-tag-destructive {
|
|
background: var(--red-bg);
|
|
color: var(--red);
|
|
border: 1px solid var(--red-bd);
|
|
}
|
|
|
|
.stage-note-inline {
|
|
color: var(--text-muted);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.stage-always-note {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Toggle switch
|
|
----------------------------------------------------------------------- */
|
|
.toggle {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 38px;
|
|
height: 22px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.toggle input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
position: absolute;
|
|
}
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: var(--border);
|
|
border-radius: 22px;
|
|
cursor: pointer;
|
|
transition: background .2s;
|
|
}
|
|
|
|
.toggle-slider::before {
|
|
content: "";
|
|
position: absolute;
|
|
width: 16px;
|
|
height: 16px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background: #fff;
|
|
border-radius: 50%;
|
|
transition: transform .2s;
|
|
}
|
|
|
|
.toggle input:checked + .toggle-slider {
|
|
background: var(--blue);
|
|
}
|
|
|
|
.toggle input:checked + .toggle-slider::before {
|
|
transform: translateX(16px);
|
|
}
|
|
|
|
.toggle input:focus-visible + .toggle-slider {
|
|
outline: 2px solid var(--blue);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Log Drawer
|
|
----------------------------------------------------------------------- */
|
|
.log-drawer {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 45vh;
|
|
min-height: 260px;
|
|
background: var(--bg-card);
|
|
border-top: 2px solid var(--border);
|
|
z-index: 150;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 -6px 32px rgba(0,0,0,0.5);
|
|
animation: drawer-slide-up 0.18s ease;
|
|
}
|
|
.log-drawer[hidden] { display: none; }
|
|
|
|
@keyframes drawer-slide-up {
|
|
from { transform: translateY(100%); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
|
|
/* Shrink table when drawer is open */
|
|
body.drawer-open .table-wrap {
|
|
max-height: calc(100vh - 205px - 45vh);
|
|
}
|
|
|
|
/* Drawer header */
|
|
.drawer-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 7px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
background: var(--bg);
|
|
}
|
|
|
|
.drawer-drive-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
min-width: 80px;
|
|
}
|
|
|
|
.drawer-devname {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-strong);
|
|
font-family: "SF Mono", "Cascadia Code", monospace;
|
|
}
|
|
|
|
.drawer-drive-meta {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 240px;
|
|
}
|
|
|
|
/* Tabs */
|
|
.drawer-tabs {
|
|
display: flex;
|
|
gap: 2px;
|
|
}
|
|
|
|
.drawer-tab {
|
|
background: none;
|
|
border: 1px solid transparent;
|
|
border-radius: 5px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
padding: 4px 12px;
|
|
transition: color 0.12s, background 0.12s;
|
|
}
|
|
.drawer-tab:hover {
|
|
color: var(--text);
|
|
background: var(--bg-card);
|
|
}
|
|
.drawer-tab.active {
|
|
color: var(--text-strong);
|
|
background: var(--bg-card);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
/* Controls */
|
|
.drawer-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-left: auto;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.autoscroll-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.autoscroll-label input { accent-color: var(--blue); cursor: pointer; }
|
|
|
|
.drawer-close {
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
transition: color 0.12s, border-color 0.12s;
|
|
}
|
|
.drawer-close:hover { color: var(--text); border-color: var(--text-muted); }
|
|
|
|
/* Body + panels */
|
|
.drawer-body {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.drawer-panel {
|
|
display: none;
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
padding: 12px 16px 20px;
|
|
}
|
|
.drawer-panel.active { display: block; }
|
|
|
|
.drawer-loading,
|
|
.drawer-empty {
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
padding: 28px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Clickable rows */
|
|
#drives-tbody tr[id^="drive-"] { cursor: pointer; }
|
|
|
|
/* Active row highlight */
|
|
tr.drawer-row-active {
|
|
background: rgba(88, 166, 255, 0.07) !important;
|
|
outline: 1px solid var(--blue-bd);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
/* ---- Burn-In tab ---- */
|
|
.drawer-job-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.drawer-job-meta {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.drawer-stages {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.drawer-stage {
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stage-row-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.stage-running .stage-row-header { background: var(--blue-bg); }
|
|
.stage-passed .stage-row-header { background: var(--green-bg); }
|
|
.stage-failed .stage-row-header { background: var(--red-bg); }
|
|
|
|
.stage-icon {
|
|
font-size: 12px;
|
|
width: 16px;
|
|
text-align: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.stage-running .stage-icon { color: var(--blue); }
|
|
.stage-passed .stage-icon { color: var(--green); }
|
|
.stage-failed .stage-icon { color: var(--red); }
|
|
.stage-cancelled .stage-icon,
|
|
.stage-pending .stage-icon { color: var(--gray); }
|
|
|
|
.stage-name-label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text);
|
|
flex: 1;
|
|
}
|
|
|
|
.stage-pct {
|
|
font-size: 12px;
|
|
color: var(--blue);
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.stage-duration {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.stage-cursor {
|
|
color: var(--blue);
|
|
font-size: 14px;
|
|
animation: blink 1s step-end infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
.stage-error-line {
|
|
padding: 7px 12px;
|
|
font-size: 12px;
|
|
color: var(--red);
|
|
font-family: "SF Mono", "Cascadia Code", monospace;
|
|
background: var(--red-bg);
|
|
border-top: 1px solid var(--red-bd);
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* ---- SMART tab ---- */
|
|
.drawer-smart-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.smart-card {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.smart-card-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.smart-progress {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.smart-progress .progress-bar { flex: 1; }
|
|
|
|
.smart-detail {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* ---- Events tab ---- */
|
|
.drawer-events {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.drawer-event {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
padding: 7px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 12px;
|
|
}
|
|
.drawer-event:last-child { border-bottom: none; }
|
|
|
|
.event-time {
|
|
color: var(--text-muted);
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.event-type {
|
|
color: var(--blue);
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.event-message {
|
|
color: var(--text);
|
|
flex: 1;
|
|
}
|
|
|
|
.event-operator {
|
|
color: var(--text-muted);
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.drawer-event.event-error .event-type { color: var(--red); }
|
|
.drawer-event.event-error .event-message { color: var(--red); }
|
|
|
|
@media (max-width: 600px) {
|
|
.drawer-smart-grid { grid-template-columns: 1fr; }
|
|
.drawer-drive-meta { display: none; }
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Stage raw log output (SSH mode)
|
|
----------------------------------------------------------------------- */
|
|
.stage-log {
|
|
font-family: "SF Mono", "Consolas", "Monaco", monospace;
|
|
font-size: 11px;
|
|
line-height: 1.5;
|
|
color: var(--text-muted);
|
|
background: var(--bg);
|
|
border-left: 2px solid var(--border);
|
|
margin: 6px 0 2px 28px;
|
|
padding: 6px 10px;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
.stage-log .log-bad-block {
|
|
color: var(--red);
|
|
font-weight: 600;
|
|
}
|
|
.stage-log .log-warn {
|
|
color: var(--yellow);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
SMART attributes table in drawer
|
|
----------------------------------------------------------------------- */
|
|
.smart-attrs {
|
|
margin-top: 12px;
|
|
border-top: 1px solid var(--border);
|
|
padding-top: 10px;
|
|
}
|
|
.smart-attrs-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
margin-bottom: 6px;
|
|
}
|
|
.smart-attr-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 3px 0;
|
|
font-size: 12px;
|
|
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
|
}
|
|
.smart-attr-row:last-child { border-bottom: none; }
|
|
.smart-attr-name { color: var(--text-muted); }
|
|
.smart-attr-val { font-family: "SF Mono", monospace; font-size: 12px; }
|
|
.smart-attr-val.attr-ok { color: var(--green); }
|
|
.smart-attr-val.attr-warn { color: var(--yellow); font-weight: 600; }
|
|
.smart-attr-val.attr-fail { color: var(--red); font-weight: 600; }
|
|
.smart-attr-raw-output {
|
|
font-family: "SF Mono", "Consolas", monospace;
|
|
font-size: 10.5px;
|
|
line-height: 1.45;
|
|
color: var(--text-muted);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 8px 10px;
|
|
margin-top: 10px;
|
|
white-space: pre;
|
|
overflow: auto;
|
|
max-height: 240px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Reset button
|
|
----------------------------------------------------------------------- */
|
|
.btn-reset {
|
|
background: transparent;
|
|
border: 1px solid color-mix(in srgb, var(--text-muted) 40%, transparent);
|
|
color: var(--text-muted);
|
|
border-radius: 5px;
|
|
padding: 3px 8px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: border-color .15s, color .15s;
|
|
}
|
|
.btn-reset:hover {
|
|
border-color: var(--yellow);
|
|
color: var(--yellow);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Pool-membership lock indicators
|
|
----------------------------------------------------------------------- */
|
|
.pool-lock-icon {
|
|
display: inline-block;
|
|
margin-right: 4px;
|
|
font-size: 12px;
|
|
color: var(--yellow);
|
|
vertical-align: baseline;
|
|
}
|
|
.pool-lock-icon.pool-lock-boot {
|
|
color: var(--red, #e25555);
|
|
}
|
|
.pool-pill {
|
|
display: inline-block;
|
|
margin-top: 3px;
|
|
padding: 1px 7px;
|
|
font-size: 10.5px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.3px;
|
|
text-transform: uppercase;
|
|
border-radius: 4px;
|
|
background: color-mix(in srgb, var(--yellow) 14%, transparent);
|
|
color: var(--yellow);
|
|
border: 1px solid color-mix(in srgb, var(--yellow) 35%, transparent);
|
|
}
|
|
.pool-pill.pool-pill-boot {
|
|
background: color-mix(in srgb, var(--red, #e25555) 16%, transparent);
|
|
color: var(--red, #e25555);
|
|
border-color: color-mix(in srgb, var(--red, #e25555) 45%, transparent);
|
|
}
|
|
.pool-pill.pool-pill-exported {
|
|
background: color-mix(in srgb, #e07a3f 16%, transparent);
|
|
color: #e07a3f;
|
|
border-color: color-mix(in srgb, #e07a3f 45%, transparent);
|
|
}
|
|
.pool-lock-icon.pool-lock-exported {
|
|
color: #e07a3f;
|
|
}
|
|
.btn-unlock {
|
|
background: transparent;
|
|
border: 1px solid color-mix(in srgb, var(--yellow) 50%, transparent);
|
|
color: var(--yellow);
|
|
border-radius: 5px;
|
|
padding: 3px 9px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: background .15s, color .15s, border-color .15s;
|
|
}
|
|
.btn-unlock:hover {
|
|
background: color-mix(in srgb, var(--yellow) 14%, transparent);
|
|
}
|
|
.btn-unlock-boot {
|
|
border-color: color-mix(in srgb, var(--red, #e25555) 55%, transparent);
|
|
color: var(--red, #e25555);
|
|
}
|
|
.btn-unlock-boot:hover {
|
|
background: color-mix(in srgb, var(--red, #e25555) 14%, transparent);
|
|
}
|
|
.btn-unlock-exported {
|
|
border-color: color-mix(in srgb, #e07a3f 55%, transparent);
|
|
color: #e07a3f;
|
|
}
|
|
.btn-unlock-exported:hover {
|
|
background: color-mix(in srgb, #e07a3f 14%, transparent);
|
|
}
|
|
.unlock-countdown {
|
|
margin-left: 4px;
|
|
font-size: 11px;
|
|
color: var(--green, #39c179);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.unlock-countdown-expired {
|
|
color: var(--yellow);
|
|
}
|
|
.modal.modal-danger {
|
|
border-top: 3px solid var(--red, #e25555);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Parallel burn-in inline warning
|
|
----------------------------------------------------------------------- */
|
|
.sf-inline-warn {
|
|
background: color-mix(in srgb, var(--yellow) 12%, transparent);
|
|
border: 1px solid color-mix(in srgb, var(--yellow) 40%, transparent);
|
|
border-radius: 5px;
|
|
color: var(--yellow);
|
|
font-size: 12px;
|
|
padding: 7px 10px;
|
|
margin: 4px 0 8px 0;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
SSH textarea
|
|
----------------------------------------------------------------------- */
|
|
.sf-textarea {
|
|
resize: vertical;
|
|
min-height: 90px;
|
|
font-family: "SF Mono", "Consolas", monospace;
|
|
font-size: 11px;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
Version badge in header
|
|
----------------------------------------------------------------------- */
|
|
.header-version {
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
opacity: .55;
|
|
font-weight: 400;
|
|
letter-spacing: 0;
|
|
align-self: flex-end;
|
|
padding-bottom: 1px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|