${credential.mode === 'apiKey' ? 'API key' : 'Service account'} | ${credential.requestsPerMinute} RPM
const state = { token: sessionStorage.getItem('adminToken') || '', config: null, discoveredSheets: [], credentialId: '', documentId: '', publicationId: '' }; const secretMask = '............'; const element = (selector) => document.querySelector(selector); const status = (message, isError = false) => { element('#status').textContent = message; element('#status').style.color = isError ? '#a83129' : '#d8562b'; }; function applyTheme(isDark) { document.documentElement.dataset.theme = isDark ? 'dark' : ''; element('#dark-mode').checked = isDark; } function applyCredentialMode() { const isApiKey = element('input[name="mode"]:checked').value === 'apiKey'; element('#api-key-field').hidden = !isApiKey; element('#service-account-field').hidden = isApiKey; element('#service-account-upload').hidden = isApiKey; } window.lucide?.createIcons(); async function adminFetch(path, options = {}) { const response = await fetch(path, { ...options, headers: { 'content-type': 'application/json', 'x-admin-token': state.token, ...options.headers } }); if (!response.ok) throw new Error((await response.json()).error || 'Request failed.'); return response.status === 204 ? null : response.json(); } function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (character) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[character]); } function selectedTitles(selector) { return [...document.querySelectorAll(`${selector} input:checked`)].map((input) => input.value); } function renderPicker(selector, sheets, selected = sheets.map((sheet) => sheet.title)) { element(selector).innerHTML = sheets.map((sheet) => ``).join(''); } function recordButtons(type, id) { return `
`; } function renderCredentials() { const credentials = state.config.credentials; element('#credentials-list').innerHTML = credentials.length ? credentials.map((credential) => `No credentials added yet.
'; element('#document-credential').innerHTML = credentials.length ? '' + credentials.map((credential) => ``).join('') : ''; } function renderDocuments() { const documents = state.config.documents; element('#documents-list').innerHTML = documents.length ? documents.map((item) => { const credential = state.config.credentials.find((entry) => entry.id === item.credentialId); return `No documents added yet.
'; element('#publication-document').innerHTML = documents.length ? '' + documents.map((item) => ``).join('') : ''; renderEndpointPicker(); } function renderEndpointPicker(selected = []) { const selectedDocument = state.config?.documents.find((item) => item.id === element('#publication-document').value); element('#endpoint-sheet-picker').innerHTML = selectedDocument ? (() => { renderPicker('#endpoint-sheet-picker', selectedDocument.sheets, selected.length ? selected : selectedDocument.sheets.map((sheet) => sheet.title)); return element('#endpoint-sheet-picker').innerHTML; })() : 'Select a document to choose its tabs.
'; } function renderPublications() { const publications = state.config.publications; element('#publications').innerHTML = publications.length ? publications.map((publication) => { const url = `${location.origin}/api/v1/${publication.id}`; return `${escapeHtml(url)}No endpoints published yet.
'; } function renderRequestFilters() { const current = element('#request-endpoint').value; element('#request-endpoint').innerHTML = '' + state.config.publications.map((publication) => ``).join(''); element('#request-endpoint').value = current; } function renderRequestLog(entries) { element('#request-log').innerHTML = entries.length ? entries.map((entry) => `No matching API requests.
'; } async function loadConfig() { state.config = await adminFetch('/admin/config'); element('#cors-origins').value = state.config.settings.corsOrigins.join('\n'); element('#trusted-proxy-hops').value = state.config.settings.trustedProxyHops; element('#metrics-url').textContent = `${location.origin}/metrics`; renderCredentials(); renderDocuments(); renderPublications(); renderRequestFilters(); bindRecordActions(); } async function loadRequests() { const query = new URLSearchParams(); if (element('#request-endpoint').value) query.set('publicationId', element('#request-endpoint').value); if (element('#request-status').value) query.set('status', element('#request-status').value); if (element('#request-result').value) query.set('result', element('#request-result').value); renderRequestLog(await adminFetch(`/admin/requests?${query}`)); } async function loadStatus() { const service = await adminFetch('/admin/status'); element('#service-status').textContent = `Healthy | ${service.uptimeSeconds}s uptime | ${service.caches.endpointEntries} endpoint cache entries | ${service.caches.tabEntries} tab cache entries`; element('#endpoint-health').innerHTML = service.endpoints.length ? service.endpoints.map((endpoint) => `No endpoints published yet.
'; await loadRequests(); } function showPage(page) { document.querySelectorAll('.page').forEach((section) => { section.hidden = section.id !== page; }); document.querySelectorAll('.tab').forEach((button) => button.classList.toggle('active', button.dataset.page === page)); if (page === 'operations') loadStatus().catch((error) => { element('#service-status').textContent = error.message; }); } function resetCredential() { state.credentialId = ''; element('#credential-name').value = ''; element('#api-key').value = ''; element('#service-account').value = ''; element('#api-key').placeholder = ''; element('#service-account').placeholder = ''; element('#rpm').value = 45; element('#save-credential').textContent = 'Add credential profile'; element('#cancel-credential').hidden = true; } function resetDocument() { state.documentId = ''; state.discoveredSheets = []; element('#document-name').value = ''; element('#document-url').value = ''; element('#sheet-picker').innerHTML = ''; element('#save-document').textContent = 'Add document'; element('#cancel-document').hidden = true; } function resetPublication() { state.publicationId = ''; element('#publication-name').value = ''; element('#cache').value = 30; element('#max-rows').value = 10000; element('#consumer-rpm').value = 120; element('#scheduled-refresh').checked = false; element('#data-format').value = 'rows'; element('#sheet-range').value = ''; element('#omit-blank-rows').checked = false; element('#publish').textContent = 'Publish endpoint'; element('#cancel-publication').hidden = true; renderEndpointPicker(); } function bindRecordActions() { document.querySelectorAll('[data-edit-credential]').forEach((button) => button.addEventListener('click', () => { const credential = state.config.credentials.find((item) => item.id === button.dataset.editCredential); state.credentialId = credential.id; element('#credential-name').value = credential.name; element('#rpm').value = credential.requestsPerMinute; element(`input[name="mode"][value="${credential.mode}"]`).checked = true; applyCredentialMode(); element('#api-key').value = credential.mode === 'apiKey' ? secretMask : ''; element('#service-account').value = credential.mode === 'serviceAccount' ? secretMask : ''; element('#save-credential').textContent = 'Save credential changes'; element('#cancel-credential').hidden = false; showPage('credentials'); status('Credential loaded. Replace the dots only to change its saved secret.'); })); document.querySelectorAll('[data-edit-document]').forEach((button) => button.addEventListener('click', () => { const item = state.config.documents.find((document) => document.id === button.dataset.editDocument); state.documentId = item.id; state.discoveredSheets = item.sheets; element('#document-name').value = item.name; element('#document-url').value = `https://docs.google.com/spreadsheets/d/${item.spreadsheetId}/edit`; element('#document-credential').value = item.credentialId; renderPicker('#sheet-picker', item.sheets); element('#save-document').textContent = 'Save document changes'; element('#cancel-document').hidden = false; showPage('documents'); status('Document loaded. Load sheet tabs again to refresh the available names.'); })); document.querySelectorAll('[data-edit-publication]').forEach((button) => button.addEventListener('click', () => { const item = state.config.publications.find((publication) => publication.id === button.dataset.editPublication); state.publicationId = item.id; element('#publication-name').value = item.name; element('#publication-document').value = item.documentId; element('#cache').value = item.cacheSeconds; element('#max-rows').value = item.maxRows; element('#consumer-rpm').value = item.consumerRequestsPerMinute; element('#scheduled-refresh').checked = Boolean(item.scheduledRefresh); element('#data-format').value = item.dataFormat || 'rows'; element('#sheet-range').value = item.range || ''; element('#omit-blank-rows').checked = Boolean(item.omitBlankRows); renderEndpointPicker(item.sheets.map((sheet) => sheet.title)); element('#publish').textContent = 'Save endpoint changes'; element('#cancel-publication').hidden = false; showPage('endpoints'); status('Endpoint loaded.'); })); document.querySelectorAll('[data-copy-token]').forEach((button) => button.addEventListener('click', async () => { try { const secret = await adminFetch(`/admin/publications/${button.dataset.copyToken}/token`); await navigator.clipboard.writeText(secret.token); button.textContent = 'Copied'; setTimeout(() => { button.textContent = 'Copy token'; }, 1_500); } catch (error) { status(error.message, true); } })); document.querySelectorAll('[data-rotate-token]').forEach((button) => button.addEventListener('click', async () => { if (!confirm('Rotate this token? Existing clients will stop working immediately.')) return; try { const secret = await adminFetch(`/admin/publications/${button.dataset.rotateToken}/rotate-token`, { method: 'POST' }); await navigator.clipboard.writeText(secret.token); button.textContent = 'New token copied'; setTimeout(() => { button.textContent = 'Rotate token'; }, 1_500); status('Token rotated and copied.'); } catch (error) { status(error.message, true); } })); document.querySelectorAll('[data-refresh-publication]').forEach((button) => button.addEventListener('click', async () => { try { await adminFetch(`/admin/publications/${button.dataset.refreshPublication}/refresh`, { method: 'POST' }); button.textContent = 'Refreshed'; setTimeout(() => { button.textContent = 'Refresh'; }, 1_500); } catch (error) { status(error.message, true); } })); document.querySelectorAll('[data-toggle-publication]').forEach((button) => button.addEventListener('click', async () => { try { const publication = state.config.publications.find((item) => item.id === button.dataset.togglePublication); await adminFetch(`/admin/publications/${publication.id}/enabled`, { method: 'POST', body: JSON.stringify({ enabled: !publication.enabled }) }); await loadConfig(); } catch (error) { status(error.message, true); } })); ['credential', 'document', 'publication'].forEach((type) => document.querySelectorAll(`[data-delete-${type}]`).forEach((button) => button.addEventListener('click', async () => { if (!confirm(`Delete this ${type}? This cannot be undone.`)) return; try { await adminFetch(`/admin/${type === 'publication' ? 'publications' : `${type}s`}/${button.dataset[`delete${type[0].toUpperCase()}${type.slice(1)}`]}`, { method: 'DELETE' }); await loadConfig(); status(`${type[0].toUpperCase()}${type.slice(1)} removed.`); } catch (error) { status(error.message, true); } }))); } element('#connect').addEventListener('click', async () => { state.token = element('#admin-token').value.trim(); try { await loadConfig(); sessionStorage.setItem('adminToken', state.token); element('#access-panel').hidden = true; element('#console').hidden = false; status('Connected.'); } catch (error) { status(error.message, true); } }); document.querySelectorAll('.tab').forEach((button) => button.addEventListener('click', () => showPage(button.dataset.page))); applyTheme(localStorage.getItem('theme') === 'dark'); applyCredentialMode(); document.querySelectorAll('input[name="mode"]').forEach((input) => input.addEventListener('change', applyCredentialMode)); element('#dark-mode').addEventListener('change', (event) => { const isDark = event.target.checked; localStorage.setItem('theme', isDark ? 'dark' : 'light'); applyTheme(isDark); }); element('#save-settings').addEventListener('click', async () => { try { const corsOrigins = element('#cors-origins').value.split(/\r?\n/); await adminFetch('/admin/settings', { method: 'PUT', body: JSON.stringify({ corsOrigins, trustedProxyHops: element('#trusted-proxy-hops').value }) }); await loadConfig(); status('CORS settings saved.'); } catch (error) { status(error.message, true); } }); element('#service-account-file').addEventListener('change', async (event) => { const [file] = event.target.files; if (file) element('#service-account').value = await file.text(); }); element('#save-credential').addEventListener('click', async () => { try { await adminFetch('/admin/credentials', { method: 'POST', body: JSON.stringify({ id: state.credentialId || undefined, name: element('#credential-name').value, mode: element('input[name="mode"]:checked').value, apiKey: element('#api-key').value === secretMask ? '' : element('#api-key').value, serviceAccountJson: element('#service-account').value === secretMask ? '' : element('#service-account').value, requestsPerMinute: element('#rpm').value }) }); await loadConfig(); resetCredential(); applyCredentialMode(); status('Credential saved.'); } catch (error) { status(error.message, true); } }); element('#cancel-credential').addEventListener('click', resetCredential); element('#discover').addEventListener('click', async () => { try { const result = await adminFetch('/admin/documents/discover', { method: 'POST', body: JSON.stringify({ credentialId: element('#document-credential').value, url: element('#document-url').value }) }); state.discoveredSheets = result.sheets; renderPicker('#sheet-picker', result.sheets); status(`Loaded ${result.sheets.length} tab(s) from ${result.title}.`); } catch (error) { status(error.message, true); } }); element('#save-document').addEventListener('click', async () => { try { const sheets = state.discoveredSheets.filter((sheet) => selectedTitles('#sheet-picker').includes(sheet.title)); await adminFetch('/admin/documents', { method: 'POST', body: JSON.stringify({ id: state.documentId || undefined, name: element('#document-name').value, url: element('#document-url').value, credentialId: element('#document-credential').value, sheets }) }); await loadConfig(); resetDocument(); status('Document saved.'); } catch (error) { status(error.message, true); } }); element('#cancel-document').addEventListener('click', resetDocument); element('#publication-document').addEventListener('change', () => renderEndpointPicker()); element('#publish').addEventListener('click', async () => { try { const selectedDocument = state.config.documents.find((item) => item.id === element('#publication-document').value); if (!selectedDocument) throw new Error('Select a document first.'); const titles = selectedTitles('#endpoint-sheet-picker'); const sheets = selectedDocument.sheets.filter((sheet) => titles.includes(sheet.title)); await adminFetch('/admin/publications', { method: 'POST', body: JSON.stringify({ id: state.publicationId || undefined, name: element('#publication-name').value, documentId: selectedDocument.id, sheets, cacheSeconds: element('#cache').value, maxRows: element('#max-rows').value, consumerRequestsPerMinute: element('#consumer-rpm').value, scheduledRefresh: element('#scheduled-refresh').checked, dataFormat: element('#data-format').value, range: element('#sheet-range').value, omitBlankRows: element('#omit-blank-rows').checked }) }); await loadConfig(); resetPublication(); status('Endpoint saved.'); } catch (error) { status(error.message, true); } }); ['#request-endpoint', '#request-status', '#request-result'].forEach((selector) => element(selector).addEventListener('change', () => loadRequests().catch((error) => { element('#request-log').textContent = error.message; }))); element('#clear-request-log').addEventListener('click', async () => { if (!confirm('Clear all retained API request logs?')) return; try { await adminFetch('/admin/requests', { method: 'DELETE' }); await loadRequests(); } catch (error) { status(error.message, true); } }); element('#rotate-admin-token').addEventListener('click', async () => { if (!confirm('Rotate the admin token? Other admin sessions will stop working.')) return; try { const result = await adminFetch('/admin/token/rotate', { method: 'POST' }); state.token = result.token; sessionStorage.setItem('adminToken', state.token); await navigator.clipboard.writeText(result.token); status('Admin token rotated and copied.'); } catch (error) { status(error.message, true); } }); element('#export-config').addEventListener('click', async () => { try { const exported = await adminFetch('/admin/config/export'); const url = URL.createObjectURL(new Blob([JSON.stringify(exported, null, 2)], { type: 'application/json' })); const link = document.createElement('a'); link.href = url; link.download = 'sheet-relay-config.json'; link.click(); URL.revokeObjectURL(url); } catch (error) { status(error.message, true); } }); element('#import-config').addEventListener('click', () => element('#config-file').click()); element('#config-file').addEventListener('change', async (event) => { const [file] = event.target.files; if (!file) return; try { const imported = JSON.parse(await file.text()); if (!confirm('Replace all existing configuration with this import?')) return; await adminFetch('/admin/config/import', { method: 'POST', body: JSON.stringify(imported) }); await loadConfig(); await loadStatus(); status('Configuration imported. Add credential secrets, then enable endpoints.'); } catch (error) { status(error.message, true); } finally { event.target.value = ''; } }); element('#cancel-publication').addEventListener('click', resetPublication); if (state.token) { loadConfig().then(() => { element('#admin-token').value = state.token; element('#access-panel').hidden = true; element('#console').hidden = false; }).catch(() => { sessionStorage.removeItem('adminToken'); state.token = ''; }); }