# 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
Generating your eSIM profile...
✓
eSIM Ready!
Your profile has been generated.
Download Profile
Next Steps:
- Download the profile above
- Go to Settings → Mobile Data → Add eSIM
- Select "Import from file"
- Disconnect from this WiFi
- Your device will connect to 5G automatically
✗
Activation Failed
An error occurred. Please try again.
```
---
## `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
```