The stored apiUrl was required to already carry the `/api` suffix, since
api.js builds requests as `${baseUrl}/credentials`. The options label read
"FC base URL", so entering the instance root -- the natural reading --
sent every request one path segment short: POST /credentials hit the Vue
SPA catch-all and came back 405, and GET /extension/manifest 404'd.
Worse, Test Connection reported success on it: the catch-all answers GET
/credentials with 200 HTML, so `r.ok` was true and the only affordance
meant to catch this misconfiguration actively masked it.
Normalize instead of validate (rules 92, 26):
- New lib/url.js: normalizeApiUrl / webRootFromApiUrl, one source shared
by the background client and the options page. Accepts either the
instance root or the API root.
- api.js normalizes on read, so configs already stored in the broken form
heal themselves without the operator reopening Settings.
- options.js stores the canonical form, echoes back what it saved, and
the test now asserts a JSON content-type -- killing the false green.
- 404/405 in request() now names the URL and points at the setting.
- Options label/placeholder state that both forms work.
Version 1.0.9 -> 1.0.10 in BOTH manifest.json and package.json; build.yml
resolves the release version from package.json, and a stale value there
would hit the cached ext-1.0.9 asset and republish the old XPI unsigned
against the new code.
Refs #2393
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
2.4 KiB
JavaScript
65 lines
2.4 KiB
JavaScript
document.addEventListener('DOMContentLoaded', async () => {
|
|
const stored = await browser.storage.local.get(['apiUrl', 'apiKey']);
|
|
document.getElementById('api-url').value = stored.apiUrl || '';
|
|
document.getElementById('api-key').value = stored.apiKey || '';
|
|
|
|
document.getElementById('save-btn').addEventListener('click', save);
|
|
document.getElementById('test-btn').addEventListener('click', test);
|
|
});
|
|
|
|
async function save() {
|
|
const apiUrl = normalizeApiUrl(document.getElementById('api-url').value);
|
|
const apiKey = document.getElementById('api-key').value.trim();
|
|
if (!apiUrl || !apiKey) {
|
|
showStatus('Both fields are required.', 'err');
|
|
return;
|
|
}
|
|
await browser.storage.local.set({ apiUrl, apiKey });
|
|
await browser.storage.local.remove(['lastConnectionTest', 'lastConnectionStatus']);
|
|
// Show what was actually stored — the operator may have typed the instance
|
|
// root and it was normalized to the API root.
|
|
document.getElementById('api-url').value = apiUrl;
|
|
showStatus(`Saved — using ${apiUrl}`, 'ok');
|
|
}
|
|
|
|
async function test() {
|
|
const apiUrl = normalizeApiUrl(document.getElementById('api-url').value);
|
|
const apiKey = document.getElementById('api-key').value.trim();
|
|
if (!apiUrl || !apiKey) {
|
|
showStatus('Fill both fields first.', 'err');
|
|
return;
|
|
}
|
|
try {
|
|
const r = await fetch(`${apiUrl}/credentials`, {
|
|
method: 'GET',
|
|
headers: { 'X-Extension-Key': apiKey },
|
|
});
|
|
if (!r.ok) {
|
|
showStatus(`HTTP ${r.status}: ${r.statusText}`, 'err');
|
|
return;
|
|
}
|
|
// A 200 is NOT sufficient. If the URL resolves to the Vue SPA instead of
|
|
// the JSON API, the catch-all route returns 200 with an HTML document —
|
|
// which used to report "Connected" on a config that could not POST at all.
|
|
const contentType = r.headers.get('content-type') || '';
|
|
if (!contentType.includes('json')) {
|
|
showStatus(
|
|
`${apiUrl} answered with ${contentType || 'no content-type'}, not JSON `
|
|
+ '— that looks like the FC web UI rather than its API.',
|
|
'err',
|
|
);
|
|
return;
|
|
}
|
|
showStatus(`Connected to ${apiUrl} — HTTP ${r.status}.`, 'ok');
|
|
} catch (e) {
|
|
showStatus(`Cannot reach ${apiUrl}: ${e.message}`, 'err');
|
|
}
|
|
}
|
|
|
|
function showStatus(text, kind) {
|
|
const el = document.getElementById('status');
|
|
el.textContent = text;
|
|
el.className = `status ${kind}`;
|
|
el.style.display = 'block';
|
|
}
|