Reworked settings page
Build and Publish / Build and Publish Docker Image (push) Successful in 3m28s
Build and Publish / Build and Publish Docker Image (push) Successful in 3m28s
This commit is contained in:
+310
-85
@@ -806,35 +806,68 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
.settings-page {
|
||||
max-width: none;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(620px, 1fr) minmax(360px, 440px);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
grid-template-areas:
|
||||
"access access access access oidc oidc oidc oidc oidc oidc oidc oidc"
|
||||
"agent agent agent agent agent agent agent agent agentstatus agentstatus agentstatus agentstatus"
|
||||
"similarity similarity similarity similarity similarity similarity similarity similarity similaritystatus similaritystatus similaritystatus similaritystatus"
|
||||
"federation federation federation federation federation federation federation federation federation federation federation federation"
|
||||
"lastfm lastfm lastfm lastfm lastfm lastfm lastfm lastfm developer developer developer developer"
|
||||
"actions actions actions actions actions actions actions actions actions actions actions actions";
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.settings-column {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
align-content: start;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.settings-side .settings-grid {
|
||||
.settings-section {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.settings-access { grid-area: access; }
|
||||
.settings-oidc { grid-area: oidc; }
|
||||
.settings-agent { grid-area: agent; }
|
||||
.settings-agent-status { grid-area: agentstatus; }
|
||||
.settings-similarity { grid-area: similarity; }
|
||||
.settings-similarity-status { grid-area: similaritystatus; }
|
||||
.settings-federation { grid-area: federation; }
|
||||
.settings-lastfm { grid-area: lastfm; }
|
||||
.settings-developer { grid-area: developer; }
|
||||
.settings-section-narrow { border-left: 2px solid rgba(29, 185, 84, 0.55); }
|
||||
|
||||
.settings-access .settings-grid,
|
||||
.settings-developer .settings-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.settings-section-narrow .panel-head {
|
||||
background: rgba(29, 185, 84, 0.035);
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
grid-column: 1 / -1;
|
||||
grid-area: actions;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: rgba(35, 35, 35, 0.96);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
gap: 14px 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
@@ -843,22 +876,26 @@ tbody tr:hover {
|
||||
|
||||
.setting-field {
|
||||
min-width: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.setting-field.settings-short { max-width: 150px; }
|
||||
.settings-wide { max-width: 680px; }
|
||||
|
||||
.setting-field label,
|
||||
.setting-toggle label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 7px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setting-field input {
|
||||
.setting-field input,
|
||||
.setting-field select {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
@@ -869,13 +906,27 @@ tbody tr:hover {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.setting-field input:focus {
|
||||
.setting-field textarea {
|
||||
width: 100%;
|
||||
min-height: 68px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.setting-field input:focus,
|
||||
.setting-field select:focus,
|
||||
.setting-field textarea:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.setting-toggle {
|
||||
min-height: 74px;
|
||||
padding: 12px;
|
||||
min-height: 68px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
@@ -894,9 +945,12 @@ tbody tr:hover {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.setting-toggle input {
|
||||
.setting-toggle input,
|
||||
.setting-toggle-row input[type="checkbox"] {
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -904,7 +958,8 @@ tbody tr:hover {
|
||||
margin-top: 6px;
|
||||
color: var(--text-subdued);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.45;
|
||||
max-width: 68ch;
|
||||
}
|
||||
|
||||
.source-pill {
|
||||
@@ -929,6 +984,101 @@ tbody tr:hover {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.settings-federation-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(360px, 4fr) minmax(580px, 8fr);
|
||||
gap: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.settings-federation-body > .settings-grid,
|
||||
.settings-federation-body > .probe-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.federation-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.transport-log {
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.transport-log-head {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 14px 0 7px;
|
||||
}
|
||||
|
||||
.transport-log-head strong { font-size: 12px; }
|
||||
.transport-log-head span { color: var(--text-subdued); font-size: 11px; }
|
||||
|
||||
.transport-log-scroll {
|
||||
max-height: 210px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 7px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.transport-row {
|
||||
display: grid;
|
||||
grid-template-columns: 68px minmax(100px, 1.2fr) 84px 72px 54px 64px 64px 66px 66px 66px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 850px;
|
||||
min-height: 30px;
|
||||
padding: 5px 9px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.055);
|
||||
color: var(--text-secondary);
|
||||
font: 11px/1.2 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transport-row:last-child { border-bottom: 0; }
|
||||
.transport-row.transport-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
color: var(--text-subdued);
|
||||
background: var(--bg-elevated);
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.transport-row > span { overflow: hidden; text-overflow: ellipsis; }
|
||||
.transport-number { text-align: right; }
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.settings-layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"access"
|
||||
"oidc"
|
||||
"agent"
|
||||
"agentstatus"
|
||||
"similarity"
|
||||
"similaritystatus"
|
||||
"federation"
|
||||
"lastfm"
|
||||
"developer"
|
||||
"actions";
|
||||
}
|
||||
.settings-federation-body { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.settings-grid,
|
||||
.federation-status-grid { grid-template-columns: 1fr; }
|
||||
.setting-field { max-width: none; }
|
||||
}
|
||||
|
||||
.settings-note {
|
||||
padding: 14px;
|
||||
color: var(--text-secondary);
|
||||
@@ -937,7 +1087,7 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
.probe-body {
|
||||
padding: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.probe-intro {
|
||||
@@ -955,9 +1105,43 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
.probe-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.probe-row strong {
|
||||
max-width: 210px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.similarity-profile-details {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.similarity-profile-details > span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: var(--text-subdued);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.similarity-profile-details pre {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.library-row {
|
||||
@@ -1560,7 +1744,7 @@ tbody tr:hover {
|
||||
<p x-text="pageSubtitle()"></p>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<button class="btn" @click="refreshAll()">
|
||||
<button class="btn" @click="refreshAll()" x-show="activeView !== 'settings'">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
Refresh
|
||||
</button>
|
||||
@@ -2059,7 +2243,7 @@ tbody tr:hover {
|
||||
<div class="settings-page">
|
||||
<form class="settings-layout" @submit.prevent="saveSettings()">
|
||||
<div class="settings-column">
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-section-wide settings-oidc">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>OIDC</strong>
|
||||
@@ -2070,6 +2254,7 @@ tbody tr:hover {
|
||||
<div class="setting-field settings-wide">
|
||||
<label>Callback URL</label>
|
||||
<input readonly :value="callbackUrl()" />
|
||||
<div class="setting-help">Register this exact redirect URL in your identity provider.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<label>
|
||||
@@ -2105,6 +2290,7 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('oidc_admin_groups')" x-text="settingSource('oidc_admin_groups')"></span>
|
||||
</label>
|
||||
<input x-model="settingsDraft.oidc_admin_groups" placeholder="/admin,/furumusic-admins" />
|
||||
<div class="setting-help">Comma-separated identity-provider groups whose members receive administrator access.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<label>
|
||||
@@ -2112,11 +2298,12 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('oidc_user_groups')" x-text="settingSource('oidc_user_groups')"></span>
|
||||
</label>
|
||||
<input x-model="settingsDraft.oidc_user_groups" />
|
||||
<div class="setting-help">Comma-separated groups allowed to sign in. Leave empty to allow any authenticated OIDC user.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-section-wide settings-agent">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>Agent</strong>
|
||||
@@ -2134,12 +2321,13 @@ tbody tr:hover {
|
||||
<input type="checkbox" x-model="settingsDraft.agent_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<div class="setting-field settings-short">
|
||||
<label>
|
||||
<span>Concurrency</span>
|
||||
<span class="source-pill" :class="sourceClass('agent_concurrency')" x-text="settingSource('agent_concurrency')"></span>
|
||||
</label>
|
||||
<input type="number" min="1" max="32" x-model="settingsDraft.agent_concurrency" />
|
||||
<div class="setting-help">Maximum number of inbox items processed at the same time. Higher values use more CPU and LLM capacity.</div>
|
||||
</div>
|
||||
<div class="setting-field settings-wide">
|
||||
<label>
|
||||
@@ -2161,6 +2349,7 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('agent_llm_url')" x-text="settingSource('agent_llm_url')"></span>
|
||||
</label>
|
||||
<input x-model="settingsDraft.agent_llm_url" />
|
||||
<div class="setting-help">Base URL of an OpenAI-compatible service. The agent sends chat requests to its <code>/v1/chat/completions</code> endpoint.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<label>
|
||||
@@ -2168,6 +2357,7 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('agent_llm_model')" x-text="settingSource('agent_llm_model')"></span>
|
||||
</label>
|
||||
<input x-model="settingsDraft.agent_llm_model" />
|
||||
<div class="setting-help">Model identifier sent to the configured LLM service, for example the name exposed by your local model server.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<label>
|
||||
@@ -2175,6 +2365,7 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('agent_llm_auth')" x-text="settingSource('agent_llm_auth')"></span>
|
||||
</label>
|
||||
<input type="password" x-model="settingsDraft.agent_llm_auth" autocomplete="off" />
|
||||
<div class="setting-help">Complete HTTP Authorization value expected by the LLM endpoint, for example <code>Bearer …</code>.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<label>
|
||||
@@ -2182,6 +2373,7 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('agent_confidence_threshold')" x-text="settingSource('agent_confidence_threshold')"></span>
|
||||
</label>
|
||||
<input x-model="settingsDraft.agent_confidence_threshold" />
|
||||
<div class="setting-help">Minimum confidence required to accept generated metadata automatically. Lower-confidence results are sent for review.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<label>
|
||||
@@ -2189,11 +2381,12 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('agent_context_limit')" x-text="settingSource('agent_context_limit')"></span>
|
||||
</label>
|
||||
<input x-model="settingsDraft.agent_context_limit" />
|
||||
<div class="setting-help">Maximum model context budget in tokens. Reduce it for smaller models or increase it when processing large batches.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-section-wide settings-similarity">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>Similarity Search</strong>
|
||||
@@ -2210,7 +2403,7 @@ tbody tr:hover {
|
||||
<span x-text="settingsDraft.similarity_enabled ? 'Enabled for this instance' : 'Disabled'"></span>
|
||||
<input type="checkbox" x-model="settingsDraft.similarity_enabled" />
|
||||
</div>
|
||||
<div class="setting-help">Downloads the selected model and processes every visible local track. With federation enabled, signed anonymous LSH summaries discover likely peers; full query embeddings are sent only to those peers, and this instance answers their searches.</div>
|
||||
<div class="setting-help">Builds an audio fingerprint index for finding musically similar tracks. When federation is enabled, compatible peers can also participate in searches.</div>
|
||||
</div>
|
||||
<div class="setting-field settings-wide">
|
||||
<label>
|
||||
@@ -2222,7 +2415,10 @@ tbody tr:hover {
|
||||
<option :value="model.id" x-text="`${model.id} · ${model.dimensions}d`"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="setting-help" x-text="(similarityStatus.models || []).find(model => model.id === settingsDraft.similarity_model)?.license || ''"></div>
|
||||
<div class="setting-help">
|
||||
<span>The model converts audio into vectors used for comparison. Changing it rebuilds the search index.</span>
|
||||
<span x-show="(similarityStatus.models || []).find(model => model.id === settingsDraft.similarity_model)?.license" x-text="' License: ' + (similarityStatus.models || []).find(model => model.id === settingsDraft.similarity_model)?.license"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-field settings-wide">
|
||||
<label>
|
||||
@@ -2234,25 +2430,22 @@ tbody tr:hover {
|
||||
<option :value="profile.id" x-text="profile.title"></option>
|
||||
</template>
|
||||
</select>
|
||||
<details class="setting-help" style="margin-top:8px">
|
||||
<summary style="cursor:pointer">Show profile details</summary>
|
||||
<pre style="white-space:pre-wrap;font:inherit;margin:8px 0 0" x-text="selectedSimilarityProfile()?.details || 'Profile details are loading…'"></pre>
|
||||
</details>
|
||||
<div class="setting-help">Controls how audio is decoded and normalized before comparison. Changing it rebuilds the search index.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<div class="setting-field settings-short">
|
||||
<label>
|
||||
<span>Background workers</span>
|
||||
<span class="source-pill" :class="sourceClass('similarity_workers')" x-text="settingSource('similarity_workers')"></span>
|
||||
</label>
|
||||
<input type="number" min="1" max="16" step="1" x-model="settingsDraft.similarity_workers" />
|
||||
<div class="setting-help">Applied immediately after saving.</div>
|
||||
<div class="setting-help">Number of tracks indexed in parallel. Higher values finish sooner but use more CPU and memory.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="settings-column settings-side">
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-section-narrow settings-access">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>Authentication</strong>
|
||||
@@ -2283,26 +2476,15 @@ tbody tr:hover {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-lastfm">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>API</strong>
|
||||
<span>Developer and enrichment integrations</span>
|
||||
<strong>Last.fm Integration</strong>
|
||||
<span>Metadata enrichment and scrobbling credentials</span>
|
||||
</div>
|
||||
<span class="badge" :class="settings.lastfm_scrobbling_configured ? 'ok' : 'disabled'" x-text="settings.lastfm_scrobbling_configured ? 'Last.fm configured' : 'Last.fm missing'"></span>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="setting-toggle">
|
||||
<label>
|
||||
<span>Swagger UI</span>
|
||||
<span class="source-pill" :class="sourceClass('swagger_enabled')" x-text="settingSource('swagger_enabled')"></span>
|
||||
</label>
|
||||
<div class="setting-toggle-row">
|
||||
<span x-text="settingsDraft.swagger_enabled ? 'Enabled' : 'Disabled'"></span>
|
||||
<input type="checkbox" x-model="settingsDraft.swagger_enabled" />
|
||||
</div>
|
||||
<div class="setting-help">Interactive API docs at /swagger/ after restart.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<label>
|
||||
<span>{{ t.settings_lastfm_api_key }}</span>
|
||||
@@ -2322,7 +2504,29 @@ tbody tr:hover {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-developer">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>Developer API</strong>
|
||||
<span>Interactive API documentation</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="setting-toggle settings-wide">
|
||||
<label>
|
||||
<span>Swagger UI</span>
|
||||
<span class="source-pill" :class="sourceClass('swagger_enabled')" x-text="settingSource('swagger_enabled')"></span>
|
||||
</label>
|
||||
<div class="setting-toggle-row">
|
||||
<span x-text="settingsDraft.swagger_enabled ? 'Available at /swagger/' : 'Disabled' "></span>
|
||||
<input type="checkbox" x-model="settingsDraft.swagger_enabled" />
|
||||
</div>
|
||||
<div class="setting-help">Exposes interactive API documentation at <code>/swagger/</code> for developers and integrations.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-section settings-section-full settings-federation">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>Federation</strong>
|
||||
@@ -2330,6 +2534,7 @@ tbody tr:hover {
|
||||
</div>
|
||||
<span class="badge" :class="federationStatus.node && federationStatus.node.running ? 'ok' : 'disabled'" x-text="federationStatus.node && federationStatus.node.running ? 'running' : 'stopped'"></span>
|
||||
</div>
|
||||
<div class="settings-federation-body">
|
||||
<div class="settings-grid">
|
||||
<div class="setting-toggle">
|
||||
<label>
|
||||
@@ -2340,7 +2545,7 @@ tbody tr:hover {
|
||||
<span x-text="settingsDraft.federation_enabled ? 'Enabled' : 'Disabled'"></span>
|
||||
<input type="checkbox" x-model="settingsDraft.federation_enabled" />
|
||||
</div>
|
||||
<div class="setting-help">Applies immediately on save — no restart needed. Peers can browse and stream every visible track.</div>
|
||||
<div class="setting-help">Lets other peers in this logical network discover the visible library and request audio streams from this instance.</div>
|
||||
</div>
|
||||
<div class="setting-field settings-wide">
|
||||
<label>
|
||||
@@ -2348,9 +2553,9 @@ tbody tr:hover {
|
||||
<span class="source-pill" :class="sourceClass('federation_network_id')" x-text="settingSource('federation_network_id')"></span>
|
||||
</label>
|
||||
<input x-model="settingsDraft.federation_network_id" placeholder="my-crew-music-7f3a" autocomplete="off" />
|
||||
<div class="setting-help">Every peer using the same id finds the others automatically.</div>
|
||||
<div class="setting-help">Peers with the same Network ID form one isolated logical network and discover each other automatically. You may choose any private value; use the exact same value on every peer that should join this network.</div>
|
||||
</div>
|
||||
<div class="setting-field">
|
||||
<div class="setting-field settings-wide">
|
||||
<label>
|
||||
<span>Save federated tracks on play</span>
|
||||
<span class="source-pill" :class="sourceClass('federation_save_on_listen')" x-text="settingSource('federation_save_on_listen')"></span>
|
||||
@@ -2359,10 +2564,11 @@ tbody tr:hover {
|
||||
<span x-text="settingsDraft.federation_save_on_listen ? 'Import into the shared library' : 'Use temporary cache'"></span>
|
||||
<input type="checkbox" x-model="settingsDraft.federation_save_on_listen" />
|
||||
</div>
|
||||
<div class="setting-help">Server-wide policy. Imported tracks become available to every user and are published by this peer. Federation metadata is trusted and bypasses the AI agent.</div>
|
||||
<div class="setting-help">When enabled, a federated track is permanently imported after playback and becomes available to every local user. Otherwise it remains only in the temporary cache. Imported peer metadata is trusted as provided and does not enter AI review.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="probe-body" x-show="federationStatus.node">
|
||||
<div class="federation-status-grid">
|
||||
<div class="probe-table" x-show="federationStatus.node && federationStatus.node.running">
|
||||
<div class="probe-row"><span>Endpoint</span><strong x-text="fedShort(federationStatus.node && federationStatus.node.endpoint_id)"></strong></div>
|
||||
<div class="probe-row"><span>Network</span><strong x-text="(federationStatus.node && federationStatus.node.network) || '-'"></strong></div>
|
||||
@@ -2372,7 +2578,7 @@ tbody tr:hover {
|
||||
<div class="probe-row"><span>Published items</span><strong x-text="(federationStatus.node && federationStatus.node.published_items) ?? '-'"></strong></div>
|
||||
<div class="probe-row"><span>Last sync</span><strong x-text="federationStatus.last_sync || 'not yet'"></strong></div>
|
||||
</div>
|
||||
<div class="probe-table" x-show="fedTransport().total_samples > 0" style="margin-top:10px">
|
||||
<div class="probe-table" x-show="fedTransport().total_samples > 0">
|
||||
<div class="probe-row">
|
||||
<span>Transport path</span>
|
||||
<strong>
|
||||
@@ -2384,23 +2590,34 @@ tbody tr:hover {
|
||||
<div class="probe-row"><span>Protocols</span><strong x-text="`${fedTransport().audio_samples || 0} audio · ${fedTransport().catalog_samples || 0} catalog · ${fedTransport().similarity_samples || 0} similarity · ${fedTransport().sync_samples || 0} sync`"></strong></div>
|
||||
<div class="probe-row"><span>Last peer</span><strong x-text="fedShort(fedTransport().last_peer)"></strong></div>
|
||||
</div>
|
||||
<div class="probe-table" x-show="fedTransport().last && fedTransport().last.length" style="margin-top:10px">
|
||||
<template x-for="(sample, index) in fedTransport().last.slice(0, 5)" :key="`${sample.at}-${sample.protocol}-${sample.direction}-${sample.phase}-${sample.peer_id}-${index}`">
|
||||
<div class="probe-row">
|
||||
<span x-text="`${sample.protocol} · ${sample.direction} · ${sample.phase}`"></span>
|
||||
<strong>
|
||||
<span class="badge" :class="fedPathBadge(sample.selected_path)" x-text="sample.selected_path || 'unknown'"></span>
|
||||
<span x-text="` ${fedRtt(sample.selected_rtt_ms)} · tx ${formatBytes(sample.total_tx_bytes || 0)} · rx ${formatBytes(sample.total_rx_bytes || 0)} · lost ${formatBytes(sample.lost_bytes || 0)}`"></span>
|
||||
</strong>
|
||||
<div class="transport-log" x-show="fedTransport().last && fedTransport().last.length">
|
||||
<div class="transport-log-head">
|
||||
<strong>Recent transport operations</strong>
|
||||
<span>Newest first · updates automatically</span>
|
||||
</div>
|
||||
<div class="transport-log-scroll">
|
||||
<div class="transport-row transport-header">
|
||||
<span>Time</span><span>User / peer</span><span>Protocol</span><span>Direction</span><span>Phase</span><span>Path</span><span class="transport-number">RTT</span><span class="transport-number">TX</span><span class="transport-number">RX</span><span class="transport-number">Lost</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-for="(sample, index) in fedTransport().last" :key="`${sample.at}-${sample.protocol}-${sample.direction}-${sample.phase}-${sample.peer_id}-${index}`">
|
||||
<div class="transport-row">
|
||||
<span :title="sample.at" x-text="formatTransportTime(sample.at)"></span>
|
||||
<span :title="sample.user_name || sample.peer_id" x-text="sample.user_name || fedShort(sample.peer_id)"></span>
|
||||
<span x-text="sample.protocol || '-'"></span>
|
||||
<span x-text="sample.direction || '-'"></span>
|
||||
<span x-text="sample.phase || '-'"></span>
|
||||
<span x-text="sample.selected_path || 'unknown'"></span>
|
||||
<span class="transport-number" x-text="fedRtt(sample.selected_rtt_ms)"></span>
|
||||
<span class="transport-number" x-text="formatBytes(sample.total_tx_bytes || 0)"></span>
|
||||
<span class="transport-number" x-text="formatBytes(sample.total_rx_bytes || 0)"></span>
|
||||
<span class="transport-number" x-text="formatBytes(sample.lost_bytes || 0)"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="probe-intro muted" x-show="federationStatus.last_error" x-text="federationStatus.last_error"></p>
|
||||
<div class="toolbar" style="margin-top:14px; flex-wrap:wrap; gap:8px">
|
||||
<button class="btn" type="button" @click="loadFederation()" :disabled="federationLoading">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="btn" type="button" @click="fedSyncNow()" :disabled="federationLoading || !(federationStatus.node && federationStatus.node.running)">
|
||||
<i data-lucide="upload-cloud"></i>
|
||||
Publish now
|
||||
@@ -2422,13 +2639,14 @@ tbody tr:hover {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-section-narrow settings-similarity-status">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>Similarity Status</strong>
|
||||
<span>Model download, indexing, and active profile</span>
|
||||
<strong>Search Index Statistics</strong>
|
||||
<span>Similarity model, indexing progress, and storage</span>
|
||||
</div>
|
||||
<span class="badge" :class="similarityBadge()" x-text="similarityStatus.status?.phase || 'disabled'"></span>
|
||||
</div>
|
||||
@@ -2445,15 +2663,15 @@ tbody tr:hover {
|
||||
<div class="probe-row"><span>Stored</span><strong x-text="`${similarityStatus.status?.stored_vectors || 0} vectors · ${formatBytes(similarityStatus.status?.stored_bytes || 0)}`"></strong></div>
|
||||
<div class="probe-row" x-show="similarityStatus.status?.current_track"><span>Current track</span><strong x-text="similarityStatus.status?.current_track"></strong></div>
|
||||
</div>
|
||||
<div class="similarity-profile-details">
|
||||
<span>Selected preprocessing profile</span>
|
||||
<pre x-text="selectedSimilarityProfile()?.details || 'Profile information is not available.'"></pre>
|
||||
</div>
|
||||
<div style="height:6px;background:rgba(255,255,255,.08);border-radius:999px;overflow:hidden;margin-top:12px" x-show="similarityStatus.status?.phase === 'processing'">
|
||||
<div style="height:100%;background:var(--accent);transition:width .25s" :style="`width:${similarityProgress()}%`"></div>
|
||||
</div>
|
||||
<p class="probe-intro muted" x-show="similarityStatus.status?.last_error" x-text="similarityStatus.status?.last_error"></p>
|
||||
<div class="toolbar" style="margin-top:14px;flex-wrap:wrap;gap:8px">
|
||||
<button class="btn" type="button" @click="loadSimilarity()" :disabled="similarityLoading">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="btn danger" type="button" @click="clearSimilarityEmbeddings()" :disabled="similarityLoading || !(similarityStatus.status?.stored_vectors > 0)">
|
||||
<i data-lucide="trash-2"></i>
|
||||
Clear all embeddings
|
||||
@@ -2462,7 +2680,7 @@ tbody tr:hover {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel settings-section settings-section-narrow settings-agent-status">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<strong>Agent Status</strong>
|
||||
@@ -2493,10 +2711,6 @@ tbody tr:hover {
|
||||
<div class="action-strip settings-actions">
|
||||
<span class="selection-summary">Settings are stored as database overrides unless an environment variable wins.</span>
|
||||
<div class="toolbar">
|
||||
<button class="btn" type="button" @click="loadSettings()">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
Reload
|
||||
</button>
|
||||
<button class="btn primary" type="submit" :disabled="settingsSaving">
|
||||
<i :data-lucide="settingsSaving ? 'loader-circle' : 'save'"></i>
|
||||
<span x-text="settingsSaving ? 'Saving...' : 'Save settings'"></span>
|
||||
@@ -2568,7 +2782,7 @@ tbody tr:hover {
|
||||
<div class="user-activity-row">
|
||||
<div class="user-activity-cover">
|
||||
<template x-if="play.cover_url">
|
||||
<img :src="play.cover_url" :alt="play.release_title || play.title" loading="lazy">
|
||||
<img :src="play.cover_url" alt="" aria-hidden="true" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!play.cover_url">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="4"/></svg>
|
||||
@@ -3497,6 +3711,17 @@ function adminV2() {
|
||||
return ms != null ? `${Math.round(Number(ms))} ms` : '-';
|
||||
},
|
||||
|
||||
formatTransportTime(value) {
|
||||
const date = new Date(value);
|
||||
if (!value || Number.isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
},
|
||||
|
||||
async loadSettingsProbe(showErrors = true) {
|
||||
this.settingsProbeLoading = true;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user