const { createApp } = Vue; const API_BASE = '/platform/v1'; createApp({ data() { return { currentTab: 'dashboard', gatewayStatus: 'Checking...', serviceKey: '', // Auth State isAuthenticated: false, isVerifyingAuth: true, isLoggingIn: false, adminPassword: '', loginError: null, // Dashboard projects: [], newProjectName: '', users: [], metrics: 'Loading...', metricsInterval: null, pillars: [], pillarInterval: null, // Functions functions: [], newFunctionName: 'hello-world', newFunctionCode: 'export default async function(req) { \n return new Response("Hello from MadBase!"); \n}', selectedFunction: null, isDeploying: false, // Database tables: [], selectedTable: null, tableData: [], // Storage buckets: [], selectedBucket: null, objects: [], // Realtime ws: null, wsConnected: false, wsChannel: 'room:lobby', wsMessages: [], wsInput: '{"event":"broadcast","payload":{"message":"Hello"}}', // Logs logQuery: '{app="gateway"}', logLimit: 100, logs: [] } }, async mounted() { console.log('MadBase Studio: Mounting...'); await this.checkAuth(); this.isVerifyingAuth = false; }, methods: { async checkAuth() { // Quick check if we can access the platform API try { const res = await fetch(`${API_BASE}/projects`); if (res.status === 401) { this.isAuthenticated = false; } else { this.isAuthenticated = true; this.onAuthenticated(); } } catch { this.isAuthenticated = false; } }, async login() { this.isLoggingIn = true; this.loginError = null; try { const res = await fetch(`${API_BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: this.adminPassword }) }); if (res.ok) { this.isAuthenticated = true; this.onAuthenticated(); } else { this.loginError = 'Invalid admin password'; } } catch (e) { this.loginError = 'Connection failed: ' + e.message; } finally { this.isLoggingIn = false; } }, async onAuthenticated() { // First fetch config (service key) await this.fetchAdminConfig(); // Then init regular data this.checkHealth(); this.fetchProjects(); this.fetchUsers(); this.fetchFunctions(); this.fetchMetrics(); this.fetchPillars(); this.initChart(); if (this.metricsInterval) clearInterval(this.metricsInterval); this.metricsInterval = setInterval(this.fetchMetrics, 5000); if (this.pillarInterval) clearInterval(this.pillarInterval); this.pillarInterval = setInterval(this.fetchPillars, 5000); console.log('MadBase Studio: Ready.'); }, async fetchAdminConfig() { try { const res = await fetch(`${API_BASE}/admin/config`); const data = await res.json(); this.serviceKey = data.service_role_key; console.log('MadBase Studio: Service Key Provisioned.'); } catch (e) { console.error('Failed to fetch admin config:', e); } }, // Dashboard async checkHealth() { try { const res = await fetch('/'); this.gatewayStatus = res.ok ? 'Online' : 'Error'; } catch { this.gatewayStatus = 'Offline'; } }, async fetchProjects() { try { const res = await fetch(`${API_BASE}/projects`); this.projects = await res.json(); } catch (e) { console.error(e); } }, async createProject() { if (!this.newProjectName) return; await fetch(`${API_BASE}/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: this.newProjectName, owner_id: null }) }); this.newProjectName = ''; this.fetchProjects(); }, async deleteProject(id) { if (!confirm('Are you sure you want to delete this project?')) return; await fetch(`${API_BASE}/projects/${id}`, { method: 'DELETE' }); this.fetchProjects(); }, async fetchUsers() { try { const res = await fetch(`${API_BASE}/users`); this.users = await res.json(); } catch (e) { console.error(e); } }, async deleteUser(id) { if (!confirm('Are you sure you want to delete this user?')) return; await fetch(`${API_BASE}/users/${id}`, { method: 'DELETE' }); this.fetchUsers(); }, async fetchMetrics() { try { const res = await fetch('/metrics'); const text = await res.text(); this.metrics = text.split('\n').filter(l => !l.startsWith('#') && l.trim()).slice(0, 20).join('\n'); // Parse for chart const match = text.match(/axum_http_requests_total\{.*?\} (\d+)/); if (match && this.chart) { const val = parseInt(match[1]); const now = new Date().toLocaleTimeString(); this.chart.data.labels.push(now); this.chart.data.datasets[0].data.push(val); if (this.chart.data.labels.length > 20) { this.chart.data.labels.shift(); this.chart.data.datasets[0].data.shift(); } this.chart.update('none'); } } catch {} }, async fetchPillars() { try { const res = await fetch(`${API_BASE}/cluster/pillars`); if (res.ok) { this.pillars = await res.json(); } } catch (e) { console.error('Pillar fetch failed:', e); } }, async scalePillar(pillar, direction) { const p = this.pillars.find(x => x.pillar === pillar); if (!p) return; const targetCount = direction === 'up' ? p.node_count + 1 : p.node_count - 1; if (targetCount < 1) return; const req = { provider: 'hetzner', // Default for now target_worker_count: pillar === 'worker' ? targetCount : undefined, target_db_count: pillar === 'database' ? targetCount : undefined, target_control_count: pillar === 'proxyapi' ? targetCount : undefined, }; try { const res = await fetch(`${API_BASE}/cluster/scale-plan`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req) }); const plan = await res.json(); if (confirm(`Scaling ${pillar} to ${targetCount} nodes. Monthly cost impact: €${plan.total_cost_monthly}. Proceed?`)) { await fetch(`${API_BASE}/cluster/scale-execute`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(plan.scaling_plan) }); this.fetchPillars(); } } catch (e) { alert('Scaling failed: ' + e.message); } }, initChart() { if (typeof Chart === 'undefined') { console.warn('MadBase Studio: Chart.js is not loaded yet.'); return; } const ctx = document.getElementById('metricsChart'); if (!ctx) return; this.chart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'HTTP Requests', data: [], borderColor: '#0ea5e9', backgroundColor: 'rgba(14, 165, 233, 0.1)', fill: true, tension: 0.4, borderWidth: 2, pointRadius: 0, pointHoverRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { beginAtZero: false, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { font: { size: 10 } } } }, animation: { duration: 0 } } }); }, // Functions async fetchFunctions() { try { const res = await fetch('/functions/v1', { headers: { 'x-project-ref': 'default', 'Authorization': `Bearer ${this.serviceKey}` } }); this.functions = await res.json(); } catch (e) { console.error(e); } }, initNewFunction() { this.selectedFunction = { name: 'new-func' }; this.newFunctionName = 'new-func'; this.newFunctionCode = 'export default async function(req) { \n return new Response("Hello!"); \n}'; }, async deployFunction() { this.isDeploying = true; try { const payload = { name: this.newFunctionName, runtime: 'deno', code_base64: btoa(this.newFunctionCode) }; const res = await fetch('/functions/v1', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-project-ref': 'default', 'Authorization': `Bearer ${this.serviceKey}` }, body: JSON.stringify(payload) }); if (res.ok) { alert('Function deployed successfully!'); this.fetchFunctions(); this.selectedFunction = null; // Clear view } else { alert('Deployment failed: ' + await res.text()); } } catch (e) { alert('Deployment error: ' + e); } finally { this.isDeploying = false; } }, async selectFunction(name) { try { const res = await fetch(`/functions/v1/${name}`, { headers: { 'x-project-ref': 'default', 'Authorization': `Bearer ${this.serviceKey}` } }); const func = await res.json(); this.selectedFunction = func; this.newFunctionName = func.name; this.newFunctionCode = atob(func.code); } catch (e) { console.error(e); } }, // Database async fetchTables() { try { const res = await fetch(`${API_BASE}/db/tables`); this.tables = await res.json(); } catch (e) { console.error(e); } }, async selectTable(t) { this.selectedTable = t; this.tableData = []; try { const res = await fetch(`${API_BASE}/db/tables/${t.schema}/${t.name}`); this.tableData = await res.json(); } catch (e) { console.error(e); } }, // Storage async fetchBuckets() { try { const res = await fetch('/storage/v1/bucket', { headers: { 'Authorization': `Bearer ${this.serviceKey}`, 'x-project-ref': 'default' } }); this.buckets = await res.json(); } catch (e) { alert('Failed to fetch buckets. Check Service Key.'); } }, async selectBucket(id) { this.selectedBucket = id; this.objects = []; try { const res = await fetch(`/storage/v1/object/list/${id}`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.serviceKey}`, 'x-project-ref': 'default' } }); this.objects = await res.json(); } catch (e) { console.error(e); } }, async uploadFile(event) { const file = event.target.files[0]; if (!file || !this.selectedBucket) return; try { await fetch(`/storage/v1/object/${this.selectedBucket}/${file.name}`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.serviceKey}`, 'x-project-ref': 'default', 'Content-Type': file.type || 'application/octet-stream' }, body: file }); this.selectBucket(this.selectedBucket); // Refresh alert('Upload successful'); } catch (e) { alert('Upload failed'); } }, getObjectUrl(name) { return `/storage/v1/object/${this.selectedBucket}/${name}?token=SERVICE_ROLE_BYPASS_NOT_IMPLEMENTED`; }, formatBytes(bytes, decimals = 2) { if (!+bytes) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KiB', 'MiB', 'GiB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; }, // Realtime toggleWs() { if (this.wsConnected) { this.ws.close(); this.wsConnected = false; this.wsMessages.push({ type: 'sys', time: new Date().toLocaleTimeString(), data: 'Disconnected' }); } else { const url = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/realtime/v1/websocket?apikey=${this.serviceKey}&vsn=1.0.0`; this.ws = new WebSocket(url); this.ws.onopen = () => { this.wsConnected = true; this.wsMessages.push({ type: 'sys', time: new Date().toLocaleTimeString(), data: 'Connected' }); this.sendJson({ event: 'phx_join', topic: this.wsChannel, payload: {}, ref: '1' }); }; this.ws.onmessage = (e) => { this.wsMessages.push({ type: 'in', time: new Date().toLocaleTimeString(), data: e.data }); if (this.wsMessages.length > 100) this.wsMessages.shift(); }; this.ws.onclose = () => { this.wsConnected = false; this.wsMessages.push({ type: 'sys', time: new Date().toLocaleTimeString(), data: 'Connection Closed' }); }; } }, sendWs() { if (!this.wsConnected) return; try { const payload = JSON.parse(this.wsInput); const msg = { event: payload.event || 'broadcast', topic: this.wsChannel, payload: payload.payload || payload, ref: '2' }; this.sendJson(msg); } catch (e) { alert('Invalid JSON'); } }, sendJson(data) { const str = JSON.stringify(data); this.ws.send(str); this.wsMessages.push({ type: 'out', time: new Date().toLocaleTimeString(), data: str }); }, // Logs async fetchLogs() { try { const params = new URLSearchParams({ query: this.logQuery, limit: this.logLimit }); const res = await fetch(`${API_BASE}/logs?${params}`); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); if (data.data && data.data.result) { this.logs = data.data.result.flatMap(r => r.values).sort((a, b) => b[0] - a[0]); } else { this.logs = []; } } catch (e) { alert('Log fetch failed: ' + e.message); } } }, watch: { currentTab(val) { if (val === 'storage' && !this.buckets.length) this.fetchBuckets(); if (val === 'functions' && !this.functions.length) this.fetchFunctions(); if (val === 'database' && !this.tables.length) this.fetchTables(); } } }).mount('#app');