Drop custom auth dialog; rely on browser Basic Auth handling

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
This commit is contained in:
c0dev0id
2026-06-30 09:32:44 +02:00
parent 86b14dfaa3
commit 67c75781d8
7 changed files with 45 additions and 164 deletions

View File

@@ -2,45 +2,30 @@ const U = window.NEUSICIAN_URLS;
export const URLS = U; export const URLS = U;
function authHeader(credentials) { export async function fetchAstLog() {
if (!credentials) return {}; const res = await fetch(U.astlog);
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),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.text(); return res.text();
} }
export async function fetchScoreText(credentials) { export async function fetchScoreText() {
const res = await fetch(U.score, { const res = await fetch(U.score);
headers: authHeader(credentials),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.text(); return res.text();
} }
export async function putScoreText(text, credentials) { export async function putScoreText(text) {
const res = await fetch(U.score, { const res = await fetch(U.score, {
method: 'PUT', method: 'PUT',
headers: { headers: { 'Content-Type': 'text/yaml' },
...authHeader(credentials),
'Content-Type': 'text/yaml',
},
body: text, body: text,
}); });
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res; return res;
} }
export async function fetchStatus(credentials) { export async function fetchStatus() {
const res = await fetch(U.status, { const res = await fetch(U.status);
headers: authHeader(credentials),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json(); return res.json();
} }

View File

@@ -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 { PaneCP } from './PaneCP.js';
import { PaneFO } from './PaneFO.js'; import { PaneFO } from './PaneFO.js';
import { PaneSubObjects } from './PaneSubObjects.js'; import { PaneSubObjects } from './PaneSubObjects.js';
import { ImportDialog } from './ImportDialog.js';
import { getKindGroups, KIND_LABEL } from '../subobject-kinds.js'; import { getKindGroups, KIND_LABEL } from '../subobject-kinds.js';
const FIXED_PANES = [ const FIXED_PANES = [
@@ -14,7 +13,6 @@ export const AppShell = {
props: ['store', 'importOnLoad'], props: ['store', 'importOnLoad'],
setup(props) { setup(props) {
const activePane = ref('cp'); const activePane = ref('cp');
const showImport = ref(false);
function focusedNode() { function focusedNode() {
const fp = props.store.focusPath; const fp = props.store.focusPath;
@@ -35,14 +33,6 @@ export const AppShell = {
if (!ps.some(p => p.id === activePane.value)) activePane.value = 'fo'; 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 () => { return () => {
const store = props.store; const store = props.store;
const ap = activePane.value; const ap = activePane.value;
@@ -54,7 +44,11 @@ export const AppShell = {
return h('div', { class: 'se-shell' }, [ return h('div', { class: 'se-shell' }, [
h('div', { class: 'se-pane-area' }, [ h('div', { class: 'se-pane-area' }, [
h('div', { class: ['se-pane', ap === 'cp' ? 'active' : null] }, 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('div', { class: ['se-pane', ap === 'fo' ? 'active' : null] },
h(PaneFO, { store })), h(PaneFO, { store })),
...subPaneNodes, ...subPaneNodes,
@@ -72,10 +66,6 @@ export const AppShell = {
store.errorMessage store.errorMessage
? h('div', { class: 'se-error', style: 'margin:0' }, store.errorMessage) ? h('div', { class: 'se-error', style: 'margin:0' }, store.errorMessage)
: null, : null,
showImport.value
? h(ImportDialog, { store, onClose: () => { showImport.value = false; } })
: null,
]); ]);
}; };
}, },

View File

@@ -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'),
]),
]),
]);
},
};

View File

@@ -1,5 +1,6 @@
import { h, ref, watch } from 'vue'; import { h, ref, watch, onMounted } from 'vue';
import { fetchScoreText, putScoreText, fetchAudioWidget, URLS } from '../api.js'; import { fetchAstLog, fetchScoreText, putScoreText, fetchAudioWidget, URLS } from '../api.js';
import { parseAstLog, buildModel } from '../ast-parser.js';
import { patchScore } from '../exporter.js'; import { patchScore } from '../exporter.js';
import { StatusPoller } from './StatusPoller.js'; import { StatusPoller } from './StatusPoller.js';
@@ -44,8 +45,10 @@ function shortView(node) {
} }
export const PaneCP = { export const PaneCP = {
props: ['store', 'onImportClick', 'onFocusFO'], props: ['store', 'importOnLoad', 'onFocusFO'],
setup(props) { setup(props) {
const importing = ref(false);
const importError = ref('');
const exporting = ref(false); const exporting = ref(false);
const exportError = ref(''); const exportError = ref('');
const audioWidgetHtml = 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() { async function doExport() {
exportError.value = ''; exportError.value = '';
exporting.value = true; exporting.value = true;
try { try {
const raw = await fetchScoreText(props.store.credentials); const raw = await fetchScoreText();
props.store.rawScoreText = raw; props.store.rawScoreText = raw;
const model = props.store.scoreModel; const model = props.store.scoreModel;
const { text: patched, log } = patchScore( const { text: patched, log } = patchScore(
raw, model.instruments, model.bars, model.info, model.articles ?? [], raw, model.instruments, model.bars, model.info, model.articles ?? [],
); );
props.store.exportLog = log; 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 }; props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 };
} catch (e) { } catch (e) {
exportError.value = e.message; exportError.value = e.message;
@@ -81,6 +99,8 @@ export const PaneCP = {
} }
} }
onMounted(() => { if (props.importOnLoad) doImport(); });
return () => { return () => {
const store = props.store; const store = props.store;
const model = store.scoreModel; const model = store.scoreModel;
@@ -97,10 +117,10 @@ export const PaneCP = {
h('div', { class: 'se-cp-header' }, [ h('div', { class: 'se-cp-header' }, [
h('button', { h('button', {
class: 'se-btn', class: 'se-btn',
disabled: store.isDirty, disabled: store.isDirty || importing.value,
title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server', title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server',
onClick: props.onImportClick, onClick: doImport,
}, '→ Import'), }, importing.value ? 'Importing…' : '→ Import'),
h('span', { class: 'se-cp-title' }, h('span', { class: 'se-cp-title' },
model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'), model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'),
model ? h('button', { model ? h('button', {
@@ -135,7 +155,8 @@ export const PaneCP = {
}) })
) : null, ) : 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, exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null,
// Export log (shown after export, cleared on next import) // Export log (shown after export, cleared on next import)

View File

@@ -8,7 +8,7 @@ export const StatusPoller = {
async function poll() { async function poll() {
try { try {
const status = await fetchStatus(props.store.credentials); const status = await fetchStatus();
props.store.synthesisStatus = status; props.store.synthesisStatus = status;
if (status.frozen) return; if (status.frozen) return;
} catch (_) { } catch (_) {

View File

@@ -4,7 +4,6 @@ export const store = reactive({
scoreModel: null, scoreModel: null,
rawScoreText: null, rawScoreText: null,
focusPath: [], focusPath: [],
credentials: null,
synthesisStatus: null, synthesisStatus: null,
isDirty: false, isDirty: false,
errorMessage: '', errorMessage: '',

View File

@@ -247,54 +247,6 @@
pointer-events: none; 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 */ /* Buttons */
.se-btn { .se-btn {