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:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 (_) {
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user