/** * Shorebird Self-Hosted Dashboard — Complete SPA Application * Vanilla JS, no framework dependencies. */ // ---- State ---- const state = { token: localStorage.getItem('shorebird_token') || null, user: null, apps: [], users: [], confirmCallback: null, }; // ---- API Client ---- const api = { base: '/api/v1', auth: '/auth', headers() { const h = { 'Content-Type': 'application/json' }; if (state.token) h['Authorization'] = `Bearer ${state.token}`; return h; }, async request(method, path, body) { const url = path.startsWith('/auth') ? path : `${this.base}${path}`; const opts = { method, headers: this.headers() }; if (body) opts.body = JSON.stringify(body); const res = await fetch(url, opts); if (res.status === 204) return null; const data = await res.json(); if (!res.ok) { if (res.status === 401) { logout(); throw new Error('Unauthorized'); } throw new Error(data.message || 'Request failed'); } return data; }, get(path) { return this.request('GET', path); }, post(path, body) { return this.request('POST', path, body); }, patch(path, body) { return this.request('PATCH', path, body); }, del(path) { return this.request('DELETE', path); }, }; // ---- Navigation ---- function navigate(page) { document.querySelectorAll('.page-section').forEach(s => s.classList.remove('active')); const section = document.getElementById(`page-${page}`); if (section) section.classList.add('active'); document.querySelectorAll('#sidebarNav a').forEach(a => a.classList.remove('active')); const link = document.querySelector(`#sidebarNav a[data-page="${page}"]`); if (link) link.classList.add('active'); const titles = { dashboard: 'Dashboard', apps: 'Apps', users: 'Users', settings: 'Settings' }; document.getElementById('pageTitle').textContent = titles[page] || page; if (page === 'dashboard') loadDashboard(); if (page === 'apps') loadApps(); if (page === 'users') loadUsers(); if (page === 'settings') loadSettings(); } // ---- Toast Notifications ---- function showToast(message, type) { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.remove(); }, 4000); } // ---- Modals ---- function openModal(id) { document.getElementById(id).classList.add('active'); } function closeModal(id) { document.getElementById(id).classList.remove('active'); } function confirmAction(title, message, callback) { document.getElementById('confirmTitle').textContent = title; document.getElementById('confirmMessage').textContent = message; state.confirmCallback = callback; openModal('confirmModal'); } function executeConfirm() { if (state.confirmCallback) state.confirmCallback(); closeModal('confirmModal'); } // ---- Auth ---- async function login(email, password) { const data = await api.request('POST', '/auth/token', { email, password }); state.token = data.token; localStorage.setItem('shorebird_token', data.token); await loadCurrentUser(); showApp(); navigate('dashboard'); } function logout() { state.token = null; state.user = null; state.apps = []; localStorage.removeItem('shorebird_token'); document.getElementById('appContainer').classList.remove('active'); document.getElementById('loginPage').style.display = 'flex'; } async function loadCurrentUser() { try { state.user = await api.get('/users/me'); document.getElementById('currentUserEmail').textContent = state.user.email || '—'; } catch (e) { // User endpoint might not be available state.user = { email: 'admin' }; } } function showApp() { document.getElementById('loginPage').style.display = 'none'; document.getElementById('appContainer').classList.add('active'); } async function register(name, email, password) { const data = await api.request('POST', '/auth/register', { name, email, password }); state.token = data.token; localStorage.setItem('shorebird_token', data.token); await loadCurrentUser(); closeModal('registerOverlay'); showApp(); navigate('dashboard'); showToast('Account created successfully', 'success'); } // ---- Dashboard ---- async function loadDashboard() { try { const appsResp = await api.get('/apps'); state.apps = appsResp.apps || []; document.getElementById('statApps').textContent = state.apps.length; document.getElementById('statPatches').textContent = '—'; // Try to get users try { const usersResp = await api.get('/admin/users'); state.users = usersResp.users || []; document.getElementById('statUsers').textContent = state.users.length; } catch { document.getElementById('statUsers').textContent = '—'; } // Try to get organizations try { const orgsResp = await api.get('/organizations'); const orgs = orgsResp.organizations || []; document.getElementById('statOrganizations').textContent = orgs.length; } catch { document.getElementById('statOrganizations').textContent = '—'; } // Recent apps table const tableDiv = document.getElementById('recentAppsTable'); if (state.apps.length === 0) { tableDiv.innerHTML = '
No apps yet. Create your first app to get started.
${a.app_id}`,
new Date(a.created_at).toLocaleDateString(),
])
);
}
} catch (e) {
showToast('Failed to load dashboard', 'error');
}
}
// ---- Apps ----
async function loadApps() {
try {
const resp = await api.get('/apps');
state.apps = resp.apps || [];
const tableDiv = document.getElementById('appsTable');
if (state.apps.length === 0) {
tableDiv.innerHTML = 'No apps yet. Create your first app.
${a.app_id}`,
new Date(a.created_at).toLocaleDateString(),
`
`,
])
);
} catch (e) {
showToast('Failed to load apps', 'error');
}
}
async function createApp(displayName) {
try {
await api.post('/apps', { display_name: displayName, organization_id: 0 });
closeModal('createAppOverlay');
showToast(`App "${displayName}" created`, 'success');
loadApps();
loadDashboard();
} catch (e) {
showToast(e.message || 'Failed to create app', 'error');
}
}
function confirmDeleteApp(appId, name) {
confirmAction('Delete App', `Are you sure you want to delete "${name}"? This action cannot be undone.`, async () => {
try {
await api.del(`/apps/${appId}`);
showToast(`App "${name}" deleted`, 'success');
loadApps();
loadDashboard();
} catch (e) {
showToast(e.message || 'Failed to delete app', 'error');
}
});
}
function openCreateAppModal() {
document.getElementById('appDisplayName').value = '';
openModal('createAppOverlay');
}
// ---- Users ----
async function loadUsers() {
try {
const resp = await api.get('/admin/users');
state.users = resp.users || [];
const tableDiv = document.getElementById('usersTable');
if (state.users.length === 0) {
tableDiv.innerHTML = 'No users found.
User management API not available. Check server configuration.
${window.location.origin}/api/v1${window.location.origin}/auth| ${h} | `; }); html += '
|---|
| ${cell} | `; }); html += '