Push Tracker
ria-ran--sim-drop/plan/06-captive-portal.md

158 lines
4.6 KiB
Markdown
Raw Permalink Normal View History

# 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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>eSIM Activation</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="card">
<div class="logo">📡</div>
<h1>Emergency 5G Network</h1>
<p class="subtitle">Activate your free eSIM to connect</p>
<!-- Idle state -->
<div id="state-idle">
<button id="btn-activate" class="btn-primary">Activate eSIM</button>
<p class="hint">Tap to generate your personal eSIM profile</p>
</div>
<!-- Loading state -->
<div id="state-loading" hidden>
<div class="spinner"></div>
<p>Generating your eSIM profile...</p>
</div>
<!-- Success state -->
<div id="state-success" hidden>
<div class="checkmark"></div>
<h2>eSIM Ready!</h2>
<p>Your profile has been generated.</p>
<a id="download-link" class="btn-primary" download="esim-profile.bin">
Download Profile
</a>
<div class="instructions">
<h3>Next Steps:</h3>
<ol>
<li>Download the profile above</li>
<li>Go to <strong>Settings → Mobile Data → Add eSIM</strong></li>
<li>Select "Import from file"</li>
<li>Disconnect from this WiFi</li>
<li>Your device will connect to 5G automatically</li>
</ol>
</div>
</div>
<!-- Error state -->
<div id="state-error" hidden>
<div class="error-icon"></div>
<h2>Activation Failed</h2>
<p id="error-message">An error occurred. Please try again.</p>
<button id="btn-retry" class="btn-secondary">Try Again</button>
</div>
</div>
<script src="/js/script.js"></script>
</body>
</html>
```
---
## `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
```