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;
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();
}

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

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 { 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)

View File

@@ -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 (_) {

View File

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

View File

@@ -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 {