From 67c75781d8d3648fda30c8151804a9dfbc867d4e Mon Sep 17 00:00:00 2001 From: c0dev0id Date: Tue, 30 Jun 2026 09:32:44 +0200 Subject: [PATCH] Drop custom auth dialog; rely on browser Basic Auth handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ImportDialog and all manual credential management. The browser handles 401 challenges natively for same-origin fetch requests — it shows its own credential prompt, retries, and caches the result for the session. No JS-level auth code is needed. - api.js: remove authHeader() and all credentials parameters - store.js: remove credentials field - ImportDialog.js: deleted - AppShell.js: remove showImport/openImport/onMounted dialog machinery; pass importOnLoad through to PaneCP - PaneCP.js: inline doImport() (was in ImportDialog); auto-triggers on mount when importOnLoad is true; shows inline import error on failure - StatusPoller.js: remove credentials arg from fetchStatus() - style.css: remove .se-import-dialog and .se-overlay blocks --- static/api.js | 31 ++++----------- static/components/AppShell.js | 22 +++-------- static/components/ImportDialog.js | 66 ------------------------------- static/components/PaneCP.js | 39 +++++++++++++----- static/components/StatusPoller.js | 2 +- static/store.js | 1 - static/style.css | 48 ---------------------- 7 files changed, 45 insertions(+), 164 deletions(-) delete mode 100644 static/components/ImportDialog.js diff --git a/static/api.js b/static/api.js index 0ff49da..c8791ad 100644 --- a/static/api.js +++ b/static/api.js @@ -2,45 +2,30 @@ const U = window.NEUSICIAN_URLS; export const URLS = U; -function authHeader(credentials) { - if (!credentials) return {}; - const b64 = btoa(`${credentials.username}:${credentials.password}`); - return { Authorization: `Basic ${b64}` }; -} - -export async function fetchAstLog(credentials) { - const res = await fetch(U.astlog, { - headers: authHeader(credentials), - }); +export async function fetchAstLog() { + const res = await fetch(U.astlog); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.text(); } -export async function fetchScoreText(credentials) { - const res = await fetch(U.score, { - headers: authHeader(credentials), - }); +export async function fetchScoreText() { + const res = await fetch(U.score); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.text(); } -export async function putScoreText(text, credentials) { +export async function putScoreText(text) { const res = await fetch(U.score, { method: 'PUT', - headers: { - ...authHeader(credentials), - 'Content-Type': 'text/yaml', - }, + headers: { 'Content-Type': 'text/yaml' }, body: text, }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res; } -export async function fetchStatus(credentials) { - const res = await fetch(U.status, { - headers: authHeader(credentials), - }); +export async function fetchStatus() { + const res = await fetch(U.status); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.json(); } diff --git a/static/components/AppShell.js b/static/components/AppShell.js index 890e8bf..30970dd 100644 --- a/static/components/AppShell.js +++ b/static/components/AppShell.js @@ -1,8 +1,7 @@ -import { h, ref, computed, onMounted, watch } from 'vue'; +import { h, ref, computed, watch } from 'vue'; import { PaneCP } from './PaneCP.js'; import { PaneFO } from './PaneFO.js'; import { PaneSubObjects } from './PaneSubObjects.js'; -import { ImportDialog } from './ImportDialog.js'; import { getKindGroups, KIND_LABEL } from '../subobject-kinds.js'; const FIXED_PANES = [ @@ -14,7 +13,6 @@ export const AppShell = { props: ['store', 'importOnLoad'], setup(props) { const activePane = ref('cp'); - const showImport = ref(false); function focusedNode() { const fp = props.store.focusPath; @@ -35,14 +33,6 @@ export const AppShell = { if (!ps.some(p => p.id === activePane.value)) activePane.value = 'fo'; }); - function openImport() { - if (!props.store.isDirty) showImport.value = true; - } - - onMounted(() => { - if (props.importOnLoad && !props.store.isDirty) showImport.value = true; - }); - return () => { const store = props.store; const ap = activePane.value; @@ -54,7 +44,11 @@ export const AppShell = { return h('div', { class: 'se-shell' }, [ h('div', { class: 'se-pane-area' }, [ h('div', { class: ['se-pane', ap === 'cp' ? 'active' : null] }, - h(PaneCP, { store, onImportClick: openImport, onFocusFO: () => { activePane.value = 'fo'; } })), + h(PaneCP, { + store, + importOnLoad: props.importOnLoad, + onFocusFO: () => { activePane.value = 'fo'; }, + })), h('div', { class: ['se-pane', ap === 'fo' ? 'active' : null] }, h(PaneFO, { store })), ...subPaneNodes, @@ -72,10 +66,6 @@ export const AppShell = { store.errorMessage ? h('div', { class: 'se-error', style: 'margin:0' }, store.errorMessage) : null, - - showImport.value - ? h(ImportDialog, { store, onClose: () => { showImport.value = false; } }) - : null, ]); }; }, diff --git a/static/components/ImportDialog.js b/static/components/ImportDialog.js deleted file mode 100644 index 9f0559f..0000000 --- a/static/components/ImportDialog.js +++ /dev/null @@ -1,66 +0,0 @@ -import { h, ref } from 'vue'; -import { fetchAstLog } from '../api.js'; -import { parseAstLog, buildModel } from '../ast-parser.js'; - -export const ImportDialog = { - props: ['store'], - emits: ['close'], - setup(props, { emit }) { - const username = ref(''); - const password = ref(''); - const error = ref(''); - const loading = ref(false); - - async function doImport() { - error.value = ''; - loading.value = true; - try { - const creds = { username: username.value, password: password.value }; - const text = await fetchAstLog(creds); - const rawTree = parseAstLog(text); - props.store.scoreModel = buildModel(rawTree); - props.store.credentials = creds; - emit('close'); - } catch (e) { - error.value = e.message; - } finally { - loading.value = false; - } - } - - function onKey(e) { - if (e.key === 'Escape') emit('close'); - if (e.key === 'Enter') doImport(); - } - - return () => h('div', null, [ - h('div', { class: 'se-overlay', onClick: () => emit('close') }), - h('div', { class: 'se-import-dialog', onKeydown: onKey, tabindex: -1 }, [ - h('h3', null, 'Import from server'), - h('label', null, 'Username'), - h('input', { - type: 'text', - value: username.value, - onInput: e => { username.value = e.target.value; }, - autocomplete: 'username', - }), - h('label', null, 'Password'), - h('input', { - type: 'password', - value: password.value, - onInput: e => { password.value = e.target.value; }, - autocomplete: 'current-password', - }), - error.value ? h('div', { class: 'se-error' }, error.value) : null, - h('div', { class: 'se-dialog-buttons' }, [ - h('button', { class: 'se-btn', onClick: () => emit('close') }, 'Cancel'), - h('button', { - class: 'se-btn se-btn-primary', - onClick: doImport, - disabled: loading.value, - }, loading.value ? 'Loading…' : 'Import'), - ]), - ]), - ]); - }, -}; diff --git a/static/components/PaneCP.js b/static/components/PaneCP.js index b26726f..dd50003 100644 --- a/static/components/PaneCP.js +++ b/static/components/PaneCP.js @@ -1,5 +1,6 @@ -import { h, ref, watch } from 'vue'; -import { fetchScoreText, putScoreText, fetchAudioWidget, URLS } from '../api.js'; +import { h, ref, watch, onMounted } from 'vue'; +import { fetchAstLog, fetchScoreText, putScoreText, fetchAudioWidget, URLS } from '../api.js'; +import { parseAstLog, buildModel } from '../ast-parser.js'; import { patchScore } from '../exporter.js'; import { StatusPoller } from './StatusPoller.js'; @@ -44,8 +45,10 @@ function shortView(node) { } export const PaneCP = { - props: ['store', 'onImportClick', 'onFocusFO'], + props: ['store', 'importOnLoad', 'onFocusFO'], setup(props) { + const importing = ref(false); + const importError = ref(''); const exporting = ref(false); const exportError = ref(''); const audioWidgetHtml = ref(''); @@ -61,18 +64,33 @@ export const PaneCP = { }, ); + async function doImport() { + if (props.store.isDirty) return; + importError.value = ''; + importing.value = true; + try { + const text = await fetchAstLog(); + props.store.scoreModel = buildModel(parseAstLog(text)); + props.store.exportLog = []; + } catch (e) { + importError.value = e.message; + } finally { + importing.value = false; + } + } + async function doExport() { exportError.value = ''; exporting.value = true; try { - const raw = await fetchScoreText(props.store.credentials); + const raw = await fetchScoreText(); props.store.rawScoreText = raw; const model = props.store.scoreModel; const { text: patched, log } = patchScore( raw, model.instruments, model.bars, model.info, model.articles ?? [], ); props.store.exportLog = log; - await putScoreText(patched, props.store.credentials); + await putScoreText(patched); props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 }; } catch (e) { exportError.value = e.message; @@ -81,6 +99,8 @@ export const PaneCP = { } } + onMounted(() => { if (props.importOnLoad) doImport(); }); + return () => { const store = props.store; const model = store.scoreModel; @@ -97,10 +117,10 @@ export const PaneCP = { h('div', { class: 'se-cp-header' }, [ h('button', { class: 'se-btn', - disabled: store.isDirty, + disabled: store.isDirty || importing.value, title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server', - onClick: props.onImportClick, - }, '→ Import'), + onClick: doImport, + }, importing.value ? 'Importing…' : '→ Import'), h('span', { class: 'se-cp-title' }, model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'), model ? h('button', { @@ -135,7 +155,8 @@ export const PaneCP = { }) ) : null, - // Export error + // Import / export errors + importError.value ? h('div', { class: 'se-error' }, importError.value) : null, exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null, // Export log (shown after export, cleared on next import) diff --git a/static/components/StatusPoller.js b/static/components/StatusPoller.js index fa6243c..7d3fc87 100644 --- a/static/components/StatusPoller.js +++ b/static/components/StatusPoller.js @@ -8,7 +8,7 @@ export const StatusPoller = { async function poll() { try { - const status = await fetchStatus(props.store.credentials); + const status = await fetchStatus(); props.store.synthesisStatus = status; if (status.frozen) return; } catch (_) { diff --git a/static/store.js b/static/store.js index 9b7522d..370ad4f 100644 --- a/static/store.js +++ b/static/store.js @@ -4,7 +4,6 @@ export const store = reactive({ scoreModel: null, rawScoreText: null, focusPath: [], - credentials: null, synthesisStatus: null, isDirty: false, errorMessage: '', diff --git a/static/style.css b/static/style.css index ef4e44d..832ef59 100644 --- a/static/style.css +++ b/static/style.css @@ -247,54 +247,6 @@ pointer-events: none; } -/* Import dialog */ -.se-import-dialog { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: #1e1e1e; - border: 1px solid #555; - padding: 1.5rem; - z-index: 1000; - min-width: 20rem; -} - -.se-import-dialog h3 { - margin: 0 0 1rem 0; - font-size: 1rem; -} - -.se-import-dialog label { - display: block; - font-size: 0.8rem; - color: #aaa; - margin-bottom: 0.2rem; -} - -.se-import-dialog input { - width: 100%; - background: #111; - border: 1px solid #444; - color: #ddd; - padding: 0.3rem; - box-sizing: border-box; - margin-bottom: 0.8rem; - font-family: monospace; -} - -.se-import-dialog .se-dialog-buttons { - display: flex; - gap: 0.5rem; - justify-content: flex-end; -} - -.se-overlay { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.6); - z-index: 999; -} /* Buttons */ .se-btn {