Files
psysonic/src/styles/components/modal.css
T
cucadmuh a63ba3c9cb fix(cover-backfill): kill idle CPU spin and offline-cache menu re-walks (#943)
* fix(cover-backfill): snapshot-diff worklist and live-tunable parallelism

Aggressive cover backfill pegged one tokio worker at ~100% on large,
fully-synced libraries while the download queues stayed empty.

- Take two snapshots once per pass — the DB catalog (single GROUP BY) and
  the on-disk cover bucket (one directory walk) — and download the
  set-difference. No per-row `stat` syscalls and no re-scan loop; the empty
  cache case (heavy backfill) costs zero per-item disk hits.
- Replace the front-loaded enumeration with a producer/consumer pipeline:
  the producer streams the catalog in chunks and feeds misses into a bounded
  channel; a fixed consumer pool keeps the download/encode pools saturated.
- Make cover backfill parallelism runtime-tunable from the Performance Probe
  (threads slider + "Run full pass now"); HTTP download and CPU encode
  semaphores resize live. Not surfaced in app settings.
- Add a "nothing changed" idle gate (catalog signature) so a settled pass is
  not re-run on every library:sync-idle, mirroring the analysis worker.
- Cancel promptly on switch to lazy: consumers bail on enabled/focus change
  and the producer feeds via try_send so a full channel cannot deadlock.
- Drop the per-item recursive disk walk from the ensure hot path.

* fix(cover-backfill): cheap idle gate, settle on 404s, transient retries

Follow-up to the snapshot-diff backfill: stop the periodic CPU spikes and the
89%-plateau wake storm on libraries whose covers can never reach 100%.

- Idle gate is now disk-free: compare only the catalog COUNT(DISTINCT) instead
  of walking ~all cover dirs on every sync-idle. "Did the server change?" never
  touches the filesystem. Clear-cache commands re-arm the gate (rearm_idle_gate)
  since a clear leaves the catalog total unchanged, and the settings UI wakes the
  active server after a clear.
- Settle the gate on any completed pass regardless of pending: remaining items
  are unfetchable-for-now (404), so the wake/sync-idle storm stops once the
  fetchable set is exhausted.
- Stop auto-clearing .fetch-failed markers every pass (it defeated the 30-min
  backoff and re-attempted 404s forever). The manual "Run full pass now" sends
  force=true to clear them and retry; wake/sync-idle/configure stay opportunistic.
- Rate-limit sync-idle passes (60s cooldown) as defence against chatty syncs.
- Retry cover downloads up to 3x with backoff on transient failures (5xx / 429 /
  network), but never on a real 4xx so missing covers don't hammer the server.

* fix(cover-cache): stop re-walking cover dirs from offline & cache menu

The settings cover-cache section polled disk usage + progress every 15s for
every server, each call doing a full recursive walk of the per-server cover
directory. On a fully populated cache this caused periodic CPU spikes whenever
that menu was open.

- mod.rs: add a 10s TTL memo around the per-server cover dir walk
  (cached_dir_usage_for_server), shared by cover_cache_stats_server and
  library_cover_progress; invalidate on clear (per-server and clear-all).
- CoverCacheStrategySection: recompute on entry only; rely on the
  cover:library-progress and cover:cache-cleared events for live updates;
  drop the per-cover cover:tier-ready refresh storm; turn the 15s loop into a
  5-minute safety net.

* fix(cover-backfill): keep emitting progress during the whole pass

The producer finishes enumerating the worklist long before the consumer pool
finishes downloading it, so progress was only emitted while feeding the channel
— the "offline & cache" menu and overlay then froze through the entire drain
phase. Replace the per-chunk emit with a 3s progress ticker that runs for the
lifetime of the pass and is aborted once the consumers drain (final accurate
emit still happens at settle).

* docs(changelog): record cover-backfill idle-CPU fix (PR #943)

Add [1.47.0] Fixed + Changed entries and a settingsCredits line for the
cover-backfill idle CPU / offline & cache menu work.
2026-06-02 04:56:34 +03:00

759 lines
16 KiB
CSS

/* ─ Modal ─ */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(17, 17, 27, 0.85);
backdrop-filter: blur(8px);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 10vh;
z-index: 9999;
animation: fadeIn 150ms ease both;
}
/* Perf probe must not blur/composite the whole app while profiling. */
.modal-overlay--perf-probe {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
background: rgba(17, 17, 27, 0.96);
animation: none;
}
.modal-content {
background: var(--ctp-surface0);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: var(--space-8);
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
position: relative;
animation: fadeIn 200ms ease both;
box-shadow: var(--shadow-lg);
}
/* Bio modal: keep title + close visible, scroll only the bio body. */
.modal-content.bio-modal {
display: flex;
flex-direction: column;
overflow: hidden;
padding-right: var(--space-6);
}
.bio-modal-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding-right: var(--space-2);
}
/* Modal CSV Import - siempre centrado */
.modal-overlay--csv {
position: fixed !important;
inset: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding-top: 0 !important;
}
.modal-close {
position: absolute;
top: var(--space-4);
right: var(--space-4);
color: var(--text-muted);
}
.modal-close:hover {
color: var(--text-primary);
}
.modal-title {
margin-bottom: 1rem;
font-family: var(--font-display);
}
.sidebar-perf-modal {
max-width: 680px;
display: flex;
flex-direction: column;
max-height: min(88vh, 860px);
overflow: hidden;
padding-bottom: var(--space-6);
}
.sidebar-perf-modal__header {
flex-shrink: 0;
padding-right: 2rem;
}
.sidebar-perf-modal__header .modal-title {
margin-bottom: 0.35rem;
}
.sidebar-perf-modal__tabs {
display: flex;
gap: 6px;
margin: 12px 0 10px;
padding: 4px;
border-radius: 12px;
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 88%, transparent);
border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
}
.sidebar-perf-modal__tab {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 10px;
border: none;
border-radius: 9px;
background: transparent;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 120ms ease, color 120ms ease;
}
.sidebar-perf-modal__tab:hover {
color: var(--text-primary);
background: color-mix(in srgb, var(--bg-hover, var(--ctp-surface1)) 70%, transparent);
}
.sidebar-perf-modal__tab--active {
color: var(--text-primary);
background: color-mix(in srgb, var(--accent) 16%, var(--bg-card, var(--ctp-base)));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent);
}
.sidebar-perf-modal__body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding-right: 2px;
}
.perf-monitor-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 2.5rem 1rem;
color: var(--text-muted);
font-size: 13px;
text-align: center;
}
.perf-monitor-empty--inline {
padding: 0.75rem 0.5rem;
font-size: 11px;
}
.perf-live-poll {
margin-bottom: 12px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 82%, transparent);
}
.perf-live-poll__title {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--text-muted) 92%, transparent);
margin-bottom: 8px;
}
.perf-live-poll__row {
display: flex;
flex-direction: column;
gap: 6px;
}
.perf-live-poll__label {
font-size: 11px;
color: var(--text-muted);
}
.perf-live-poll__value {
font-variant-numeric: tabular-nums;
color: var(--text-primary);
}
.perf-live-poll__row input[type='range'] {
width: 100%;
}
.perf-live-poll__row--check {
flex-direction: row;
align-items: center;
gap: 8px;
margin-top: 10px;
cursor: pointer;
user-select: none;
}
.perf-live-poll__check-label {
font-size: 11px;
color: var(--text-primary);
}
.perf-live-poll__run {
margin-top: 10px;
align-self: flex-start;
padding: 4px 10px;
font-size: 11px;
border-radius: 6px;
border: 1px solid var(--border-subtle, color-mix(in srgb, var(--text-muted) 30%, transparent));
background: color-mix(in srgb, var(--text-muted) 12%, transparent);
color: var(--text-primary);
cursor: pointer;
}
.perf-live-poll__run:disabled {
opacity: 0.6;
cursor: default;
}
.perf-live-poll__run:not(:disabled):hover {
background: color-mix(in srgb, var(--text-muted) 20%, transparent);
}
.perf-live-poll__hint {
margin: 6px 0 0;
font-size: 10px;
line-height: 1.35;
color: color-mix(in srgb, var(--text-muted) 88%, transparent);
}
.perf-metric-section {
margin-bottom: 10px;
border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
border-radius: 12px;
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 82%, transparent);
overflow: hidden;
}
.perf-metric-section__title {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 12px;
cursor: pointer;
list-style: none;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-muted);
user-select: none;
}
.perf-metric-section__title::-webkit-details-marker {
display: none;
}
.perf-metric-section[open] .perf-metric-section__chevron {
transform: rotate(90deg);
}
.perf-metric-section__chevron {
flex-shrink: 0;
transition: transform 140ms ease;
}
.perf-metric-section__hint {
margin-left: auto;
font-size: 10px;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: color-mix(in srgb, var(--text-muted) 88%, transparent);
}
.perf-metric-section__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 8px;
padding: 0 10px 10px;
}
.perf-metric-card {
padding: 10px 10px 9px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--text-muted) 14%, transparent);
background: color-mix(in srgb, var(--ctp-mantle, var(--ctp-base)) 72%, transparent);
min-height: 84px;
display: flex;
flex-direction: column;
gap: 4px;
}
.perf-metric-card--disabled {
opacity: 0.55;
}
.perf-metric-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
}
.perf-metric-card__label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
line-height: 1.25;
}
.perf-metric-card__pin {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
border-radius: 7px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
}
.perf-metric-card__pin:hover {
color: var(--text-primary);
background: color-mix(in srgb, var(--bg-hover) 80%, transparent);
}
.perf-metric-card__pin--active {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.perf-metric-card__value-row {
display: flex;
align-items: baseline;
gap: 4px;
}
.perf-metric-card__value {
font-size: 22px;
line-height: 1;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--text-primary);
}
.perf-metric-card__unit {
font-size: 11px;
color: var(--text-muted);
}
.perf-metric-card__bar-track {
height: 4px;
border-radius: 999px;
background: color-mix(in srgb, var(--text-muted) 16%, transparent);
overflow: hidden;
}
.perf-metric-card__bar-fill {
height: 100%;
border-radius: inherit;
}
.perf-metric-card__bar-fill--cpu {
background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 70%, #6cf), var(--accent));
}
.perf-metric-card__bar-fill--memory {
background: linear-gradient(90deg, #7aa2f7, #9ece6a);
}
.perf-metric-card__bar-fill--rate {
background: linear-gradient(90deg, #fab387, #f9e2af);
}
.perf-metric-card__bar-fill--neutral {
background: var(--text-muted);
}
.perf-metric-card__detail {
font-size: 10px;
line-height: 1.35;
color: color-mix(in srgb, var(--text-muted) 92%, transparent);
}
.perf-metric-card__badge {
margin-top: auto;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent);
}
.perf-toggle-tree__hint {
margin-bottom: 10px;
}
.perf-toggle-tree {
display: flex;
flex-direction: column;
gap: 8px;
}
.perf-tree-group {
border: 1px solid color-mix(in srgb, var(--text-muted) 16%, transparent);
border-radius: 10px;
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 78%, transparent);
overflow: hidden;
}
.perf-tree-group__head {
width: 100%;
display: flex;
align-items: center;
gap: 6px;
padding: 9px 10px;
padding-left: calc(10px + (var(--perf-tree-depth, 0) * 12px));
border: none;
background: transparent;
color: var(--text-primary);
font-size: 13px;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.perf-tree-group__head:hover {
background: color-mix(in srgb, var(--bg-hover) 65%, transparent);
}
.perf-tree-group__chevron {
flex-shrink: 0;
transition: transform 140ms ease;
}
.perf-tree-group__chevron--open {
transform: rotate(90deg);
}
.perf-tree-group__body {
padding-bottom: 4px;
}
.perf-tree-leaf-wrap {
padding-left: calc(18px + (var(--perf-tree-depth, 0) * 12px));
padding-right: 8px;
}
.perf-tree-leaf {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 7px 8px;
border-radius: 8px;
cursor: pointer;
}
.perf-tree-leaf:hover {
background: color-mix(in srgb, var(--bg-hover) 55%, transparent);
}
.perf-tree-leaf input[type="checkbox"] {
margin-top: 2px;
accent-color: var(--accent);
width: 15px;
height: 15px;
flex-shrink: 0;
}
.perf-tree-leaf__text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.perf-tree-leaf__label {
font-size: 13px;
color: var(--text-primary);
line-height: 1.3;
}
.perf-tree-leaf__desc {
font-size: 11px;
color: var(--text-muted);
line-height: 1.35;
}
.sidebar-perf-modal__actions {
flex-shrink: 0;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid color-mix(in srgb, var(--text-muted) 16%, transparent);
}
.perf-overlay-mode {
margin-bottom: 12px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 82%, transparent);
}
.perf-overlay-mode__title {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--text-muted) 92%, transparent);
margin-bottom: 8px;
}
.perf-overlay-mode__segments {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.perf-overlay-mode__segment {
padding: 6px 8px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--text-muted) 22%, transparent);
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 70%, transparent);
color: var(--text-muted);
font-size: 11px;
line-height: 1.2;
cursor: pointer;
}
.perf-overlay-mode__segment:hover {
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
color: var(--text-primary);
}
.perf-overlay-mode__segment--active {
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
background: color-mix(in srgb, var(--accent) 14%, var(--bg-card, var(--ctp-base)));
color: var(--text-primary);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 24%, transparent);
}
.perf-overlay-mode__hint {
margin: 8px 0 0;
font-size: 10px;
line-height: 1.35;
color: color-mix(in srgb, var(--text-muted) 92%, transparent);
}
.perf-overlay-appearance {
margin-bottom: 12px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 82%, transparent);
}
.perf-overlay-appearance__title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 10px;
}
.perf-overlay-appearance__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.perf-overlay-appearance__row:last-child {
margin-bottom: 0;
}
.perf-overlay-appearance__label {
font-size: 12px;
color: var(--text-primary);
flex-shrink: 0;
}
.perf-overlay-appearance__value {
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.perf-overlay-appearance__corners {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.perf-overlay-appearance__corner {
position: relative;
width: 52px;
height: 36px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--text-muted) 22%, transparent);
background: color-mix(in srgb, var(--ctp-mantle, var(--ctp-base)) 70%, transparent);
cursor: pointer;
padding: 0;
}
.perf-overlay-appearance__corner:hover {
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.perf-overlay-appearance__corner--active {
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
background: color-mix(in srgb, var(--accent) 12%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
}
.perf-overlay-appearance__corner-mark {
position: absolute;
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--accent);
opacity: 0.85;
}
.perf-overlay-appearance__corner-mark--top-right {
top: 6px;
right: 6px;
}
.perf-overlay-appearance__corner-mark--top-left {
top: 6px;
left: 6px;
}
.perf-overlay-appearance__corner-mark--bottom-right {
bottom: 6px;
right: 6px;
}
.perf-overlay-appearance__corner-mark--bottom-left {
bottom: 6px;
left: 6px;
}
.perf-overlay-appearance__opacity {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
.perf-overlay-appearance__opacity input[type='range'] {
width: 100%;
accent-color: var(--accent);
}
.sidebar-perf-modal__hint {
margin: 0 0 12px;
font-size: 13px;
color: var(--text-muted);
}
.sidebar-perf-modal__item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 13px;
color: var(--text-primary);
}
.sidebar-perf-modal__item input[type="checkbox"] {
accent-color: var(--accent);
width: 15px;
height: 15px;
}
.sidebar-perf-modal__phase {
margin-top: 10px;
border: 1px solid color-mix(in srgb, var(--text-muted) 22%, transparent);
border-radius: 10px;
padding: 8px 10px 10px;
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 78%, transparent);
}
.sidebar-perf-modal__phase--nested {
margin-left: 10px;
margin-right: 2px;
padding-top: 6px;
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 68%, transparent);
}
.sidebar-perf-modal__phase-title {
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
user-select: none;
list-style: none;
}
.sidebar-perf-modal__phase-title::-webkit-details-marker {
display: none;
}
.sidebar-perf-modal__cpu {
margin: 0 0 10px;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--text-muted) 22%, transparent);
border-radius: 10px;
background: color-mix(in srgb, var(--bg-card, var(--ctp-base)) 80%, transparent);
}
.sidebar-perf-modal__cpu-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 6px;
}
.sidebar-perf-modal__cpu-row {
font-size: 13px;
color: var(--text-primary);
line-height: 1.45;
}
.sidebar-perf-modal__cpu-row--indent {
padding-left: 0.75rem;
color: var(--text-muted);
}
.sidebar-perf-modal__subhead {
margin-top: 10px;
margin-bottom: 2px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
color: var(--text-muted);
}
.sidebar-perf-modal__actions {
margin-top: 14px;
display: flex;
justify-content: flex-end;
gap: 8px;
}