A
8672b0de6b
Full working closed-loop eSIM provisioning stack (spec in claude.md / plan/): Flask API, SQLite subscriber store, credential/profile generator, Open5GS and free5gc core adapters, captive portal, operational scripts, systemd units, network configs, and a 44-test suite. Profile generation currently returns a mock JSON profile; real SGP.22 generation via pySim is the planned Phase-2 upgrade. Fixes two bugs found while getting the suite green: - generate_imsi hardcoded a 10-digit MSIN, producing 16-digit IMSIs for a 3-digit MNC; MSIN length is now 15 - len(mcc) - len(mnc). - check_rate_limit compared timestamps as strings across mismatched formats (SQLite CURRENT_TIMESTAMP vs Python isoformat); both sides now normalized via SQLite datetime() so the window check is chronologically correct. Add .gitignore for venv, caches, runtime config.yaml, and databases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.6 KiB
4.6 KiB
Unit 06 — Captive Portal Frontend
Goal
Mobile-first single-page UI served at /activate. Pure HTML/CSS/JS — no external dependencies.
Files
portal/index.htmlportal/css/style.cssportal/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:
<!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
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
# 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