Add web server, SSE handlers, and UI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 11:53:21 -04:00
parent f243f3004e
commit 0318aeb08d
5 changed files with 483 additions and 1 deletions
+102
View File
@@ -0,0 +1,102 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AntiDrift</title>
<style>
:root { color-scheme: dark; }
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; background: #11131a; color: #e6e8ee; }
main { max-width: 640px; margin: 8vh auto; padding: 0 24px; }
h1 { font-size: 14px; letter-spacing: .2em; text-transform: uppercase; color: #7a8095; }
.card { background: #1a1d27; border: 1px solid #272b38; border-radius: 14px; padding: 28px; }
label { display: block; font-size: 13px; color: #9aa0b4; margin: 16px 0 6px; }
input { width: 100%; box-sizing: border-box; padding: 10px 12px; background: #11131a;
border: 1px solid #2d3242; border-radius: 8px; color: #e6e8ee; font: inherit; }
button { margin-top: 22px; padding: 11px 18px; border: 0; border-radius: 8px;
background: #4c6ef5; color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
button:disabled { background: #2d3242; color: #6a7186; cursor: not-allowed; }
.timer { font-size: 56px; font-weight: 700; font-variant-numeric: tabular-nums; margin: 8px 0 4px; }
.action { font-size: 22px; font-weight: 600; }
.meta { color: #9aa0b4; }
.pill { display: inline-block; font-size: 12px; letter-spacing: .15em; text-transform: uppercase;
color: #7a8095; border: 1px solid #272b38; border-radius: 999px; padding: 3px 10px; }
</style>
</head>
<body>
<main>
<h1>AntiDrift</h1>
<div class="card" id="view">connecting…</div>
</main>
<script>
const view = document.getElementById('view');
let countdownTimer = null;
function post(path, body) {
return fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
}
function fmt(secs) {
secs = Math.max(0, Math.floor(secs));
const m = String(Math.floor(secs / 60)).padStart(2, '0');
const s = String(secs % 60).padStart(2, '0');
return `${m}:${s}`;
}
function render(state) {
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
const rs = state.runtime_state;
if (rs === 'locked') {
view.innerHTML = `<span class="pill">Locked</span>
<p class="meta">No active commitment.</p>
<button id="plan">Start planning</button>`;
document.getElementById('plan').onclick = () => post('/planning');
} else if (rs === 'planning') {
view.innerHTML = `<span class="pill">Planning</span>
<label>Next action</label><input id="na" placeholder="What exactly will you do?">
<label>Success condition</label><input id="sc" placeholder="How do you know it's done?">
<label>Minutes</label><input id="mins" type="number" min="1" value="25">
<button id="start" disabled>Start commitment</button>`;
const na = document.getElementById('na'), sc = document.getElementById('sc'),
mins = document.getElementById('mins'), start = document.getElementById('start');
const check = () => { start.disabled = !(na.value.trim() && sc.value.trim() && +mins.value > 0); };
[na, sc, mins].forEach(el => el.oninput = check);
start.onclick = () => post('/commitment', {
next_action: na.value.trim(),
success_condition: sc.value.trim(),
timebox_secs: Math.round(+mins.value * 60),
});
} else if (rs === 'active') {
const c = state.commitment || {};
view.innerHTML = `<span class="pill">Active</span>
<div class="timer" id="t">--:--</div>
<div class="action">${c.next_action || ''}</div>
<p class="meta">Done when: ${c.success_condition || ''}</p>
<button id="done">Complete</button>`;
document.getElementById('done').onclick = () => post('/complete');
const t = document.getElementById('t');
const tick = () => { t.textContent = fmt((c.deadline_unix_secs || 0) - Date.now() / 1000); };
tick();
countdownTimer = setInterval(tick, 250);
} else if (rs === 'review') {
const c = state.commitment || {};
view.innerHTML = `<span class="pill">Review</span>
<p class="action">Session ended</p>
<p class="meta">${c.next_action || ''}</p>
<button id="end">End</button>`;
document.getElementById('end').onclick = () => post('/end');
} else {
view.textContent = rs;
}
}
const es = new EventSource('/events');
es.onmessage = (e) => render(JSON.parse(e.data));
es.onerror = () => { view.textContent = 'disconnected — is antidriftd running?'; };
</script>
</body>
</html>