Files
CTXD/app/src/ctxd/landing.html
T

310 lines
8.1 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CTXD — Context Dossier</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #1a1a1a;
--paper: #1e1e1e;
--ink: #d4d4d4;
--ink-dim: #888;
--accent: #e5c07b;
--accent3: #98c379;
--border: #2a2a2a;
--border-light: #333;
--input-bg: #222;
--hover: #252525;
--danger: #e06c75;
--font: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'IBM Plex Mono', monospace;
}
html { height: 100%; font-size: 15px; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
height: 100vh;
height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.landing {
max-width: 420px;
width: 100%;
text-align: center;
}
.landing h1 {
font-size: 1.4rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--accent);
margin-bottom: 0.35rem;
}
.landing .subtitle {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--ink-dim);
margin-bottom: 2rem;
}
.landing .description {
font-size: 0.75rem;
color: var(--ink-dim);
line-height: 1.6;
margin-bottom: 2rem;
text-align: left;
}
.landing .description code {
color: var(--accent);
background: var(--input-bg);
padding: 0.1rem 0.3rem;
font-size: 0.7rem;
}
.login-card {
background: var(--paper);
border: 1px solid var(--border-light);
padding: 1.5rem;
text-align: left;
}
.login-card h2 {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: 1rem;
}
.login-card label {
display: block;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-dim);
margin-bottom: 0.3rem;
margin-top: 0.75rem;
}
.login-card input {
width: 100%;
font-family: var(--font);
font-size: 0.8rem;
padding: 0.55rem 0.65rem;
background: var(--input-bg);
border: 1px solid var(--border);
color: var(--ink);
outline: none;
}
.login-card input:focus { border-color: var(--accent); }
.login-card .actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.login-card button {
font-family: var(--font);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 0.5rem 0.85rem;
cursor: pointer;
border: 1px solid var(--border-light);
background: none;
color: var(--ink);
}
.login-card button:hover { background: var(--hover); }
.login-card button.primary {
background: var(--accent);
color: var(--bg);
border-color: var(--accent);
flex: 1;
}
.login-card button.primary:hover { background: #d4ae5c; }
.toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
background: var(--paper);
border: 1px solid var(--border-light);
color: var(--ink);
font-family: var(--font);
font-size: 0.7rem;
padding: 0.6rem 1rem;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
z-index: 200;
}
.toast.show { opacity: 1; }
.toast.error { border-color: var(--danger); color: var(--danger); }
.toast.success { border-color: var(--accent3); color: var(--accent3); }
.links {
margin-top: 1.5rem;
font-size: 0.65rem;
color: var(--ink-dim);
line-height: 1.8;
}
.links a {
color: var(--accent);
text-decoration: none;
}
.links a:hover { text-decoration: underline; }
.status {
margin-top: 1rem;
font-size: 0.6rem;
color: var(--ink-dim);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.status .dot {
display: inline-block;
width: 5px; height: 5px;
border-radius: 50%;
background: var(--accent3);
margin-right: 0.3rem;
vertical-align: middle;
}
</style>
</head>
<body>
<div class="landing">
<h1>CTXD</h1>
<div class="subtitle">Context Dossier</div>
<div class="description">
Single source of truth for multi-harness project context.
One canonical <code>AGENTS.md</code> per project, served to
Claude, Hermes, Codex, Cursor, and any OAuth-capable MCP client.
</div>
<div class="login-card" id="login-card">
<h2>sign in</h2>
<label>user id</label>
<input type="text" id="user-id" placeholder="e.g. admin" autocomplete="username" autocorrect="off" autocapitalize="off" spellcheck="false" onkeydown="if(event.key==='Enter')submitLogin()">
<label>password</label>
<input type="password" id="password" placeholder="password" autocomplete="current-password" onkeydown="if(event.key==='Enter')submitLogin()">
<div class="actions">
<button class="primary" onclick="submitLogin()">sign in</button>
</div>
</div>
<div class="links">
<a href="/.well-known/oauth-authorization-server">OAuth discovery</a> &middot;
<a href="/readonly/sse">read-only MCP</a> &middot;
<a href="/write/sse">write MCP</a>
</div>
<div class="status" id="status">
<span class="dot"></span> connected
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API = '';
function showToast(msg, type) {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = 'toast show' + (type ? ' ' + type : '');
clearTimeout(window._tt);
window._tt = setTimeout(() => el.classList.remove('show'), 3500);
}
async function submitLogin() {
const uid = document.getElementById('user-id').value.trim();
const pw = document.getElementById('password').value;
if (!uid || !pw) {
showToast('user id and password required', 'error');
return;
}
try {
const res = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: uid, password: pw }),
});
if (res.ok) {
const data = await res.json();
localStorage.setItem('ctxd_session_token', data.token);
showToast('signed in — loading dashboard', 'success');
setTimeout(() => { window.location.href = '/'; }, 500);
} else {
let detail = '';
try { detail = (await res.json()).error || ''; } catch (_) {}
if (res.status === 401) {
showToast(detail === 'invalid credentials' ? 'invalid user id or password' : ('login failed: ' + (detail || res.status)), 'error');
} else if (res.status === 403 && detail === 'account inactive') {
showToast('account inactive — contact an admin', 'error');
} else {
showToast('login failed (' + res.status + ')', 'error');
}
}
} catch (e) {
showToast('network error — check connection', 'error');
}
}
// Check if already signed in (cookie-based — server will redirect to dashboard)
// The "signed in" card is only shown if the server served the landing page
// despite a valid cookie, which shouldn't happen. If it does, offer a redirect.
(async () => {
// If the server served the landing page, we're not authenticated via cookie.
// Try localStorage token as fallback (for backward compat with old sessions).
const token = localStorage.getItem('ctxd_session_token');
if (token) {
try {
const res = await fetch('/auth/me', { headers: { Authorization: 'Bearer ' + token } });
if (res.ok) {
// Token works via Bearer but cookie wasn't set — force redirect with token in cookie
// Re-login to get the cookie set, or just redirect (the dashboard JS uses Bearer too)
document.getElementById('login-card').innerHTML = '<h2>signed in</h2><p style="font-size:0.75rem;color:var(--ink-dim);margin-bottom:0.75rem">You are signed in.</p><div class="actions"><button class="primary" onclick="window.location.href=\'/\'">open dashboard</button></div>';
}
} catch (_) {}
}
})();
// Status check
(async () => {
try {
await fetch('/status');
document.getElementById('status').innerHTML = '<span class="dot"></span> connected';
} catch (_) {
document.getElementById('status').innerHTML = 'disconnected';
}
})();
</script>
</body>
</html>