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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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 { 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)
|
||||
|
||||
@@ -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 (_) {
|
||||
|
||||
@@ -4,7 +4,6 @@ export const store = reactive({
|
||||
scoreModel: null,
|
||||
rawScoreText: null,
|
||||
focusPath: [],
|
||||
credentials: null,
|
||||
synthesisStatus: null,
|
||||
isDirty: false,
|
||||
errorMessage: '',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user