Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,22 @@ CVE-2025-32434
# executes. Trivy's check does not account for the gosu privilege-drop
# pattern.
DS-0002

# CVE-2026-63374: anyio's TLSStream encodes host names with IDNA 2003, which
# can be made to disagree with IDNA 2008 and spoof a TLS certificate. Fixed in
# anyio 4.14.2; the lock pins 4.13.0.
#
# StemDeck never opens a TLSStream. anyio is here transitively: starlette uses
# it for task groups and thread offloading, not for sockets, and httpx (which
# does use it for TLS) is a dev-only dependency, present for FastAPI's
# TestClient. The server itself speaks plain HTTP on localhost, and the two
# things that do fetch over TLS, yt-dlp and audio-separator, bring their own
# HTTP stacks.
#
# Not fixed in place because the fix is not a one-line bump: regenerating the
# lock for it also rewrites platform markers across a dozen packages, including
# the Intel macOS paths that pyproject gates by hand, and any uv.lock change
# sends every existing desktop install to a full download instead of an in-app
# update. Take the bump the next time the lock moves for another reason, and
# drop this entry then.
CVE-2026-63374
32 changes: 19 additions & 13 deletions static/css/daw.css
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,6 @@ input, textarea { font-family: inherit; }
background: color-mix(in srgb, var(--color) 14%, transparent);
color: var(--color);
}
.stem-dot {
width: 5px; height: 5px;
border-radius: 50%;
background: currentColor;
opacity: 0.85;
}
.stem-choice[aria-pressed="false"] .stem-dot { opacity: 0.4; }
/* Automatic song-structure detection. It sits at the end of the composer next
to Split stems and matches its footprint, so the pair reads as two segments
of one bar rather than a chip loose beside a button. Colour still separates
Expand Down Expand Up @@ -392,12 +385,17 @@ input, textarea { font-family: inherit; }
.daw-iconbtn:active { transform: scale(0.96); }

/* Notification panel */
/* The wrapper is still the badge's anchor; the panel is not anchored to it.
.sidebar is overflow:hidden and narrows to the rail's 66px when the library
is collapsed, so an absolutely positioned panel would be clipped to nothing
in that state. Fixed escapes the clip, and placeNotifPanel in ui-chrome.js
sets left/top from the bell's rect, the same way the search dropdown and the
metronome popover are placed. No coordinates here on purpose: the only code
that shows this panel is the code that positions it. */
.daw-notif-wrap { position: relative; }
.daw-notif-panel {
display: none;
position: absolute;
top: calc(100% + 8px);
right: 0;
position: fixed;
width: 280px;
background: var(--panel-2);
border: 1px solid var(--border-strong);
Expand Down Expand Up @@ -772,9 +770,14 @@ input, textarea { font-family: inherit; }
.cat-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.cat-title { color: var(--fg); font-size: 12px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cat-sub { color: var(--muted); font-size: 10.5px; display: flex; gap: 5px; white-space: nowrap; overflow: hidden; }
.cat-status { width: 6px; height: 6px; border-radius: 50%; background: #5fbc56; flex-shrink: 0; }
.cat-status.processing { background: var(--accent); animation: cat-pulse 1.4s infinite; }
.cat-status.unavailable { background: #666; }
/* Shown only when it has something to say (#636). A finished import is the
resting state of every row in the library, so a green dot on all of them
marked nothing out: it was one more thing to read past on the way to the
title. The states that still carry information keep the dot, which is also
what makes them visible now that they are the only rows wearing one. */
.cat-status { display: none; width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.cat-status.processing { display: block; background: var(--accent); animation: cat-pulse 1.4s infinite; }
.cat-status.unavailable { display: block; background: #666; }
.cat-item.unavailable { cursor: pointer; }
.cat-item.unavailable .cat-title { opacity: 0.55; }
.cat-unavailable-warning { color: var(--accent); font-weight: 600; opacity: 1; }
Expand All @@ -791,6 +794,9 @@ input, textarea { font-family: inherit; }
.cat-item.queue-waiting .cat-thumb { opacity: 0.55; }
.cat-item.queue-waiting .cat-title { color: var(--muted); }
.cat-item.queue-waiting .cat-status {
/* Explicit, not inherited from .processing: a waiting row is only styled
through its parent, so it must turn the dot on for itself. */
display: block;
background: transparent;
box-shadow: inset 0 0 0 1.5px var(--muted);
animation: none;
Expand Down
99 changes: 51 additions & 48 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<div class="daw-stem-chips">
<button class="stem-choice stem-choice-all" type="button" id="stemAllBtn" aria-pressed="true" data-i18n="extract.all">All</button>
<button class="stem-choice" type="button" data-stem="vocals" aria-pressed="true" style="--color:var(--vocals)">
<span class="stem-dot"></span><span data-i18n="stem.vocals">Vocals</span>
<span data-i18n="stem.vocals">Vocals</span>
</button>
<!-- On-demand lead/backing vocal split (#275). Shown only while
Vocals is selected above; toggling "Lead + Backing" auto-runs
Expand All @@ -91,19 +91,19 @@
<button type="button" class="vocal-mode-btn" data-mode="split" aria-pressed="false" data-i18n="vocalMode.split">Lead + Backing</button>
</div>
<button class="stem-choice" type="button" data-stem="drums" aria-pressed="true" style="--color:var(--drums)">
<span class="stem-dot"></span><span data-i18n="stem.drums">Drums</span>
<span data-i18n="stem.drums">Drums</span>
</button>
<button class="stem-choice" type="button" data-stem="bass" aria-pressed="true" style="--color:var(--bass)">
<span class="stem-dot"></span><span data-i18n="stem.bass">Bass</span>
<span data-i18n="stem.bass">Bass</span>
</button>
<button class="stem-choice" type="button" data-stem="guitar" aria-pressed="true" style="--color:var(--guitar)">
<span class="stem-dot"></span><span data-i18n="stem.guitar">Guitar</span>
<span data-i18n="stem.guitar">Guitar</span>
</button>
<button class="stem-choice" type="button" data-stem="piano" aria-pressed="true" style="--color:var(--piano)">
<span class="stem-dot"></span><span data-i18n="stem.piano">Piano</span>
<span data-i18n="stem.piano">Piano</span>
</button>
<button class="stem-choice" type="button" data-stem="other" aria-pressed="false" style="--color:var(--other)">
<span class="stem-dot"></span><span data-i18n="stem.other">Other</span>
<span data-i18n="stem.other">Other</span>
</button>
</div>
</div>
Expand All @@ -114,7 +114,7 @@
without a restart. -->
<button class="stem-choice structure-toggle" type="button" id="autoSectionsBtn" aria-pressed="false" title="Experimental Song Structure Extraction" aria-label="Experimental Song Structure Extraction" data-i18n-title="structure.toggleTitle" data-i18n-aria-label="structure.toggleTitle">
<span class="structure-toggle-main">
<span class="stem-dot"></span><span data-i18n="structure.toggle">Song structure</span>
<span data-i18n="structure.toggle">Song structure</span>
</span>
<span class="structure-toggle-note" data-i18n="structure.experimental">Experimental</span>
</button>
Expand Down Expand Up @@ -175,41 +175,6 @@
</button>
</div>
</div><!-- /.daw-composer-stack -->

<!-- Notification bell -->
<div class="daw-notif-wrap" style="position:relative;flex-shrink:0;">
<button class="daw-iconbtn daw-notif-btn" id="notifBtn" title="Notifications" type="button" aria-label="Notifications" aria-expanded="false" data-i18n-title="notif.bell" data-i18n-aria-label="notif.bell">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0"/>
</svg>
<span class="daw-notif-badge hidden" id="notifBadge" aria-hidden="true"></span>
</button>
<div class="daw-notif-panel" role="menu" aria-label="Notifications" data-i18n-aria-label="notif.title">
<div class="daw-notif-header">
<span class="uplabel" data-i18n="notif.title">Notifications</span>
<button class="daw-notif-clear" id="notifClearAll" type="button" data-i18n="notif.clearAll">Clear all</button>
<button class="daw-notif-close" type="button" aria-label="Close notifications" data-i18n-aria-label="notif.close">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="daw-notif-list" id="notifList">
<div class="daw-notif-card daw-notif-release hidden" id="notifReleaseCard">
<div class="daw-notif-card-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z"/></svg>
</div>
<div class="daw-notif-card-body">
<div class="daw-notif-card-title" data-i18n="notif.newRelease">New release available</div>
<div class="daw-notif-card-desc" id="notifReleaseDesc"></div>
</div>
<button class="daw-notif-dismiss" id="notifReleaseDismiss" type="button" aria-label="Dismiss notification" data-i18n-aria-label="notif.dismiss">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<p class="daw-notif-empty" id="notifEmpty" data-i18n="notif.empty">No new notifications</p>
</div>
</div>
</div>

<!-- Hidden collapsed strip (JS compatibility) -->
<div class="hidden" id="appbarStrip" aria-hidden="true" style="display:none!important">
<div><div class="strip-sq-stems" id="appbarStripStems"></div></div>
Expand Down Expand Up @@ -255,19 +220,57 @@
<span data-i18n="nav.trash">Trash</span>
</button>
<div style="flex:1"></div>
<!-- Notifications (#636). In the rail rather than at the end
of the top bar: that row is the import and split controls, so
a bell parked on it read as part of that job. Everything else
here is app-wide rather than track-wide, which is what a
notification is. -->
<div class="daw-notif-wrap">
<button class="rail-btn" id="notifBtn" title="Notifications" type="button" aria-label="Notifications" aria-expanded="false" data-i18n-title="notif.bell" data-i18n-aria-label="notif.bell">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0"/>
</svg>
<span data-i18n="notif.title">Notifications</span>
<span class="daw-notif-badge hidden" id="notifBadge" aria-hidden="true"></span>
</button>
<div class="daw-notif-panel" role="menu" aria-label="Notifications" data-i18n-aria-label="notif.title">
<div class="daw-notif-header">
<span class="uplabel" data-i18n="notif.title">Notifications</span>
<button class="daw-notif-clear" id="notifClearAll" type="button" data-i18n="notif.clearAll">Clear all</button>
<button class="daw-notif-close" type="button" aria-label="Close notifications" data-i18n-aria-label="notif.close">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="daw-notif-list" id="notifList">
<div class="daw-notif-card daw-notif-release hidden" id="notifReleaseCard">
<div class="daw-notif-card-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z"/></svg>
</div>
<div class="daw-notif-card-body">
<div class="daw-notif-card-title" data-i18n="notif.newRelease">New release available</div>
<div class="daw-notif-card-desc" id="notifReleaseDesc"></div>
</div>
<button class="daw-notif-dismiss" id="notifReleaseDismiss" type="button" aria-label="Dismiss notification" data-i18n-aria-label="notif.dismiss">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<p class="daw-notif-empty" id="notifEmpty" data-i18n="notif.empty">No new notifications</p>
</div>
</div>
</div>
<button class="rail-btn rail-recommend" id="friendsBtn" type="button" title="We Recommend" aria-label="We Recommend" aria-haspopup="dialog" data-i18n-title="nav.weRecommend" data-i18n-aria-label="nav.weRecommend">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/>
</svg>
<span><span data-i18n="nav.weRecommendLine1">We</span><br><span data-i18n="nav.weRecommendLine2">Recommend</span></span>
</button>
<button class="rail-btn" id="settingsBtn" type="button" aria-label="Settings" aria-haspopup="dialog" data-i18n-aria-label="nav.settings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<circle cx="12" cy="12" r="3"/>
<path d="M12 2v2 M12 20v2 M4 12H2 M22 12h-2 M5 5l1.5 1.5 M17.5 17.5 19 19 M5 19l1.5-1.5 M17.5 6.5 19 5"/>
</svg>
<span data-i18n="nav.settings">Settings</span>
</button>
<button class="rail-btn rail-recommend" id="friendsBtn" type="button" title="We Recommend" aria-label="We Recommend" aria-haspopup="dialog" data-i18n-title="nav.weRecommend" data-i18n-aria-label="nav.weRecommend">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/>
</svg>
<span><span data-i18n="nav.weRecommendLine1">We</span><br><span data-i18n="nav.weRecommendLine2">Recommend</span></span>
</button>
<button class="rail-btn" id="aboutBtn" type="button" title="Help" aria-label="Help" data-i18n-title="nav.help" data-i18n-aria-label="nav.help">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<circle cx="12" cy="12" r="9"/>
Expand Down
34 changes: 34 additions & 0 deletions static/js/ui-chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,46 @@ document.getElementById("uploadFileBtn")?.addEventListener("click", () => {
// Notification panel: toggle / close / close-on-outside-click.
const notifBtn = document.getElementById("notifBtn");
const notifWrap = notifBtn?.closest(".daw-notif-wrap");
const notifPanel = notifWrap?.querySelector(".daw-notif-panel");

/**
* Put the panel beside the bell, top edges level.
*
* It cannot simply hang off .daw-notif-wrap. The bell lives in the library
* rail, .sidebar is overflow:hidden, and it narrows to the rail's 66px when
* the library is collapsed, so an absolutely positioned panel is clipped to
* nothing in that state. Fixed escapes the clip and pays for it by needing its
* coordinates set here, which is the same trade the search dropdown and the
* metronome popover already make.
*
* Measured while shown: a display:none panel has no height to clamp against.
*/
function placeNotifPanel() {
if (!notifBtn || !notifPanel) return;
const r = notifBtn.getBoundingClientRect();
// Clear of the rail, not of the button: the button is 40px centred in a 66px
// column, so measuring from its own right edge tucks the panel back under
// the rail by the leftover margin.
const column = notifBtn.closest(".sidebar-rail") || notifBtn;
notifPanel.style.left = `${Math.round(column.getBoundingClientRect().right + 10)}px`;
// Level with the bell, and only pulled up from there if the panel would
// otherwise hang off the bottom of a short window.
const lowest = window.innerHeight - notifPanel.offsetHeight - 12;
notifPanel.style.top = `${Math.round(Math.max(12, Math.min(r.top, lowest)))}px`;
}

function setNotifOpen(open) {
notifWrap?.classList.toggle("open", open);
notifBtn?.setAttribute("aria-expanded", String(open));
if (open) placeNotifPanel();
}

// The bell does not move with the window, but the clamp above depends on the
// window's height, so a resize while the panel is open can strand it.
window.addEventListener("resize", () => {
if (notifWrap?.classList.contains("open")) placeNotifPanel();
});

notifBtn?.addEventListener("click", () => {
setNotifOpen(!notifWrap?.classList.contains("open"));
});
Expand Down
Loading
Loading