# Unit 06 — Captive Portal Frontend ## Goal Mobile-first single-page UI served at `/activate`. Pure HTML/CSS/JS — no external dependencies. ## Files - `portal/index.html` - `portal/css/style.css` - `portal/js/script.js` --- ## UI States ``` [idle] → "Activate eSIM" button visible [loading] → spinner, button disabled [success] → green checkmark, download link, instructions [error] → red message, "Try Again" button ``` --- ## `portal/index.html` Structure: ```html eSIM Activation

Emergency 5G Network

Activate your free eSIM to connect

Tap to generate your personal eSIM profile

``` --- ## `portal/css/style.css` Key rules: - `body`: `font-family: system-ui, sans-serif; background: #1a1a2e; color: #eee; display: flex; align-items: center; justify-content: center; min-height: 100vh;` - `.card`: `background: #16213e; border-radius: 16px; padding: 2rem; max-width: 400px; width: 90%; text-align: center; box-shadow: 0 8px 32px rgba(0,0,0,0.4);` - `.btn-primary`: `background: #0f3460; color: white; padding: 1rem 2rem; border-radius: 8px; font-size: 1.2rem; border: none; cursor: pointer; width: 100%; margin: 1rem 0;` - `.btn-primary:hover`: `background: #e94560;` - `.spinner`: CSS animation rotating border - `.checkmark`: `color: #4caf50; font-size: 4rem;` - `.error-icon`: `color: #f44336; font-size: 4rem;` - `.instructions`: `text-align: left; background: #0f3460; border-radius: 8px; padding: 1rem; margin-top: 1rem;` --- ## `portal/js/script.js` ```javascript const states = ['idle', 'loading', 'success', 'error']; function showState(name) { states.forEach(s => { document.getElementById(`state-${s}`).hidden = (s !== name); }); } async function activate() { showState('loading'); try { const res = await fetch('/api/provision', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mac_address: '' }) // server resolves from ARP }); if (res.status === 429) { showError('You already have an active eSIM for this network.'); return; } if (!res.ok) { const err = await res.json().catch(() => ({})); showError(err.error || 'Server error. Please try again.'); return; } const data = await res.json(); document.getElementById('download-link').href = data.profile_url; showState('success'); } catch (e) { showError('Could not reach the server. Are you connected to the WiFi?'); } } function showError(msg) { document.getElementById('error-message').textContent = msg; showState('error'); } document.getElementById('btn-activate').addEventListener('click', activate); document.getElementById('btn-retry').addEventListener('click', () => { showState('idle'); }); ``` --- ## Verification ```bash # With Flask running open http://localhost:5000/activate # Verify page loads, button works, cycling through all states # Check mobile responsiveness with browser DevTools device emulation ```