personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Init page: branding overhaul, trust copy, accessibility fixes

- Comfortaa @font-face, 140px wordmark, h1 heading with letter-spacing
- "your private AI co-brain" tagline, WCAG AA link/footer colors
- Trust notes for password hash and Gemini key storage
- Rewritten section hints with lowercase voice
- Agent cards as <button> with repo_path commands
- Observer empty state with solstone.app/observers link
- <html lang="en">, <main> landmark, aria-live on field status
- disabled/tabindex management for locked sections
- Gemini API key show/hide toggle
- repo_path template variable from root.py init() route

+79 -43
+2 -1
convey/root.py
··· 125 125 return redirect(url_for("root.index")) 126 126 127 127 config_path = str(Path(get_journal()) / "config" / "journal.json") 128 - return render_template("init.html", config_path=config_path) 128 + repo_path = str(Path(__file__).resolve().parent.parent) 129 + return render_template("init.html", config_path=config_path, repo_path=repo_path) 129 130 130 131 131 132 @bp.route("/init/password", methods=["POST"])
+77 -42
convey/templates/init.html
··· 1 1 <!DOCTYPE html> 2 - <html> 2 + <html lang="en"> 3 3 <head> 4 4 <meta charset="utf-8"/> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1"/> 6 6 <title>set up — solstone</title> 7 + <link rel="icon" href="/favicon.ico"> 7 8 <style> 9 + @font-face { 10 + font-family: 'Comfortaa'; 11 + src: url('/static/Comfortaa-Variable.woff2') format('woff2'); 12 + font-display: swap; 13 + } 8 14 body { 9 15 min-height: 100vh; display: flex; align-items: center; justify-content: center; 10 - font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 1em; background: #fff; 16 + font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 1em; background: #fff; color: #555; 11 17 } 12 - svg[aria-label="sol logo"] { width: 64px; height: 64px; margin: 0 auto 1rem; display: block; } 18 + svg[aria-label="sol logo"] { width: 140px; height: 140px; margin: 0 auto 1rem; display: block; } 13 19 .init-container { max-width: 520px; width: 90vw; } 14 - .config-path { font-size: 0.75rem; color: #aaa; text-align: center; margin: 0 0 1.5rem; word-break: break-all; } 15 - h2 { text-align: center; font-weight: 500; margin-bottom: 2rem; } 20 + .config-path { font-size: 0.75rem; color: #767676; text-align: center; margin: 0 0 1.5rem; word-break: break-all; } 21 + h1 { text-align: center; font-family: 'Comfortaa', system-ui, sans-serif; font-weight: 700; letter-spacing: 0.1em; color: #222; font-size: 2.5rem; text-transform: lowercase; margin-bottom: 0.5rem; } 22 + .product-tagline { text-align: center; font-family: 'Comfortaa', system-ui, sans-serif; color: #888; text-transform: lowercase; margin: 0 0 2rem; font-size: 1rem; } 16 23 .init-section { margin-bottom: 2rem; } 17 24 .init-section.disabled { opacity: 0.4; pointer-events: none; } 18 25 .init-section h3 { font-size: 1rem; font-weight: 500; border-bottom: 1px solid #eee; padding-bottom: 0.5rem; margin-bottom: 0.75rem; } 19 26 .section-hint { font-size: 0.85rem; color: #666; margin: 0 0 0.75rem; } 20 - .section-hint a { color: #E8923A; } 27 + .section-hint a { color: #b06a1a; } 21 28 .field-group { margin-bottom: 0.75rem; } 22 29 .field-group label { display: block; font-size: 0.85rem; color: #444; margin-bottom: 0.25rem; } 23 30 .field-group input { ··· 34 41 display: inline-block; background: #f5f5f5; border-radius: 6px; 35 42 padding: 6px 12px; font-size: 0.85rem; color: #666; 36 43 } 37 - .agent-cards { display: flex; gap: 0.75rem; margin-top: 0.5rem; } 44 + .agent-cards { display: flex; gap: 0.75rem; margin-top: 0.5rem; flex-wrap: wrap; } 38 45 .agent-card { 39 46 flex: 1; padding: 1rem; border: 2px solid #ddd; border-radius: 8px; 40 47 cursor: pointer; text-align: center; transition: border-color 0.2s, background 0.2s; ··· 44 51 .agent-card.selected { border-color: #E8923A; background: #fef3e2; } 45 52 .agent-card strong { font-size: 0.95rem; } 46 53 .agent-card span { font-size: 0.8rem; color: #666; } 54 + button.agent-card { background: none; border: 2px solid #ddd; font: inherit; cursor: pointer; } 47 55 .observer-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0; } 48 56 .observer-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } 49 57 .observer-dot.connected { background: #22c55e; } ··· 56 64 border: none; background: transparent; color: #666; cursor: pointer; font-size: 0.8rem; 57 65 padding: 0; 58 66 } 59 - .footer-note { font-size: 0.8rem; color: #aaa; margin-top: 1.5rem; text-align: center; } 67 + .trust-note { font-size: 0.8rem; color: #888; margin: 0.25rem 0 0.5rem; } 68 + .observer-empty { font-size: 0.85rem; color: #888; } 69 + .observer-empty a { color: #b06a1a; } 70 + .footer-note { font-size: 0.88rem; color: #767676; margin-top: 1.5rem; text-align: center; } 60 71 </style> 61 72 </head> 62 73 <body> 63 - <div class="init-container"> 74 + <main> 75 + <div class="init-container"> 64 76 <svg xmlns="http://www.w3.org/2000/svg" viewBox="2.5 2.5 27 27" role="img" aria-label="sol logo"> 65 77 <title>sol</title> 66 78 <!-- Sun rays: 10 floating wedges with curved inner arc matching the ring --> ··· 71 83 <path fill="#E8923A" fill-rule="evenodd" d="M12.079 18.795C13.489 18.795 14.229 18.065 14.229 17.155C14.229 16.365 13.729 15.835 12.229 15.535C11.149 15.315 10.939 15.095 10.939 14.725C10.939 14.345 11.399 14.135 11.989 14.135C12.499 14.135 12.859 14.235 13.199 14.555C13.399 14.745 13.729 14.815 13.949 14.665C14.159 14.505 14.169 14.255 13.989 14.035C13.589 13.545 12.889 13.245 12.009 13.245C10.989 13.245 9.959 13.735 9.959 14.755C9.959 15.525 10.529 16.075 11.879 16.335C12.919 16.525 13.249 16.815 13.239 17.215C13.229 17.615 12.809 17.895 12.039 17.895C11.429 17.895 10.889 17.625 10.659 17.375C10.469 17.175 10.189 17.125 9.929 17.335C9.699 17.515 9.659 17.825 9.859 18.035C10.299 18.475 11.149 18.795 12.079 18.795Z M16.999 18.795C18.609 18.795 19.749 17.645 19.749 16.025C19.739 14.395 18.599 13.245 16.999 13.245C15.379 13.245 14.239 14.395 14.239 16.025C14.239 17.645 15.379 18.795 16.999 18.795ZM16.999 17.895C15.959 17.895 15.219 17.125 15.219 16.025C15.219 14.925 15.959 14.145 16.999 14.145C18.039 14.145 18.769 14.925 18.769 16.025C18.769 17.125 18.039 17.895 16.999 17.895Z M21.569 18.755H21.589C21.989 18.755 22.269 18.545 22.269 18.255C22.269 17.965 22.079 17.755 21.819 17.755H21.569C21.279 17.755 21.069 17.405 21.069 16.905V11.445C21.069 11.155 20.859 10.945 20.569 10.945C20.279 10.945 20.069 11.155 20.069 11.445V16.905C20.069 17.985 20.689 18.755 21.569 18.755Z"/> 72 84 </svg> 73 85 <p class="config-path">{{ config_path }}</p> 74 - <h2>set up solstone</h2> 86 + <h1>set up solstone</h1> 87 + <p class="product-tagline">your private AI co-brain</p> 75 88 76 89 <section class="init-section" id="section-password"> 77 90 <h3>1. set a password</h3> 78 - <p class="section-hint">Required for remote access. At least 8 characters.</p> 91 + <p class="section-hint">protects your solstone web interface. minimum 8 characters. you can reset it anytime with <code>sol password set</code>.</p> 92 + <p class="trust-note">this hash is stored on your machine and never transmitted</p> 79 93 <div class="field-group"> 80 - <label for="password">Password</label> 94 + <label for="password">password</label> 81 95 <div class="input-wrap"> 82 96 <input type="password" id="password" autocomplete="new-password"> 83 97 <button type="button" class="toggle-btn" id="toggle-password" onclick="togglePassword()">show</button> 84 98 </div> 85 - <small class="field-status">&nbsp;</small> 99 + <small class="field-status" aria-live="polite">&nbsp;</small> 86 100 </div> 87 101 </section> 88 102 ··· 90 104 <h3>2. who are you?</h3> 91 105 <p class="section-hint">Optional — helps sol address you correctly.</p> 92 106 <div class="field-group"> 93 - <label for="name">Full name</label> 94 - <input type="text" id="name"> 95 - <small class="field-status">&nbsp;</small> 107 + <label for="name">full name</label> 108 + <input type="text" id="name" disabled> 109 + <small class="field-status" aria-live="polite">&nbsp;</small> 96 110 </div> 97 111 <div class="field-group"> 98 - <label for="preferred">Preferred name</label> 99 - <input type="text" id="preferred"> 100 - <small class="field-status">&nbsp;</small> 112 + <label for="preferred">preferred name</label> 113 + <input type="text" id="preferred" disabled> 114 + <small class="field-status" aria-live="polite">&nbsp;</small> 101 115 </div> 102 116 <div class="field-group"> 103 - <label>Timezone</label> 117 + <label>timezone</label> 104 118 <span class="timezone-chip" id="timezone-display">Detecting…</span> 105 119 </div> 106 120 </section> 107 121 108 122 <section class="init-section disabled" id="section-provider"> 109 123 <h3>3. connect gemini</h3> 110 - <p class="section-hint">Optional — powers sol's thinking. Get a key at <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">aistudio.google.com/apikey</a></p> 124 + <p class="section-hint">optional — powers sol's thinking. gemini is the best-supported provider for full media processing. get a key at <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" tabindex="-1">aistudio.google.com/apikey</a> — or add other providers later in settings.</p> 125 + <p class="trust-note">your key is stored locally in your journal config</p> 111 126 <div class="field-group"> 112 127 <label for="gemini-key">Gemini API key</label> 113 - <input type="password" id="gemini-key"> 114 - <small class="field-status">&nbsp;</small> 128 + <div class="input-wrap"> 129 + <input type="password" id="gemini-key" disabled> 130 + <button type="button" class="toggle-btn" id="toggle-gemini" onclick="toggleGeminiKey()">show</button> 131 + </div> 132 + <small class="field-status" aria-live="polite">&nbsp;</small> 115 133 </div> 116 134 </section> 117 135 118 136 <section class="init-section disabled" id="section-finalize"> 119 137 <h3>4. get started</h3> 120 - <div id="observer-status" style="display:none"> 138 + <p class="section-hint">solstone is in active development. a coding agent is the best way to get your system fully running — it can set up your observer, configure providers, and help you explore.</p> 139 + <div id="observer-status"> 121 140 <p class="section-hint">Connected observers</p> 122 141 <div id="observer-list"></div> 142 + <p class="observer-empty" id="observer-empty">no observer connected yet — <a href="https://solstone.app/observers" tabindex="-1">set up an observer</a> to start capturing</p> 123 143 </div> 124 144 <div class="agent-cards"> 125 - <div class="agent-card" data-agent="claude-code" onclick="selectAgent(this)"> 145 + <button type="button" class="agent-card" data-agent="claude-code" onclick="selectAgent(this)"> 126 146 <strong>Claude Code</strong> 127 - <span>Using Claude Code</span> 128 - </div> 129 - <div class="agent-card" data-agent="codex" onclick="selectAgent(this)"> 147 + <span><code>cd {{ repo_path }} &amp;&amp; claude "help me set up solstone"</code></span> 148 + </button> 149 + <button type="button" class="agent-card" data-agent="codex" onclick="selectAgent(this)"> 130 150 <strong>Codex</strong> 131 - <span>Using Codex</span> 132 - </div> 133 - <div class="agent-card" data-agent="none" onclick="selectAgent(this)"> 134 - <strong>Skip</strong> 135 - <span>Set this up later</span> 136 - </div> 151 + <span><code>cd {{ repo_path }} &amp;&amp; codex "help me set up solstone"</code></span> 152 + </button> 153 + <button type="button" class="agent-card" data-agent="none" onclick="selectAgent(this)"> 154 + <strong>I'll set it up myself</strong> 155 + <span>see docs/INSTALL.md</span> 156 + </button> 137 157 </div> 138 158 </section> 139 159 140 160 <p class="footer-note">your data stays on your machine</p> 141 - </div> 161 + </div> 162 + </main> 142 163 143 164 <script> 144 165 function togglePassword() { 145 166 const input = document.getElementById('password'); 146 167 const btn = document.getElementById('toggle-password'); 168 + if (input.type === 'password') { input.type = 'text'; btn.textContent = 'hide'; } 169 + else { input.type = 'password'; btn.textContent = 'show'; } 170 + } 171 + 172 + function toggleGeminiKey() { 173 + const input = document.getElementById('gemini-key'); 174 + const btn = document.getElementById('toggle-gemini'); 147 175 if (input.type === 'password') { input.type = 'text'; btn.textContent = 'hide'; } 148 176 else { input.type = 'password'; btn.textContent = 'show'; } 149 177 } ··· 155 183 if (!small) return; 156 184 small.classList.remove('status-saved', 'status-error', 'status-fade'); 157 185 if (type === 'saved') { 158 - small.textContent = message || 'Saved'; 186 + small.textContent = message || 'saved'; 159 187 small.classList.add('status-saved'); 160 188 setTimeout(() => { 161 189 small.classList.add('status-fade'); 162 190 setTimeout(() => { small.textContent = '\u00a0'; small.classList.remove('status-saved', 'status-fade'); }, 300); 163 191 }, 1500); 164 192 } else { 165 - small.textContent = message || 'Error'; 193 + small.textContent = message || 'error'; 166 194 small.classList.add('status-error'); 167 195 } 168 196 } 169 197 170 198 function unlockSections() { 171 - ['section-identity', 'section-provider', 'section-finalize'].forEach(id => 172 - document.getElementById(id).classList.remove('disabled')); 199 + ['section-identity', 'section-provider', 'section-finalize'].forEach(id => { 200 + const section = document.getElementById(id); 201 + section.classList.remove('disabled'); 202 + section.querySelectorAll('input[disabled]').forEach(inp => inp.disabled = false); 203 + section.querySelectorAll('[tabindex="-1"]').forEach(el => el.removeAttribute('tabindex')); 204 + }); 173 205 const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; 174 206 document.getElementById('timezone-display').textContent = tz; 175 207 fetch('/init/identity', { ··· 182 214 document.getElementById('password').addEventListener('blur', async function() { 183 215 const password = this.value; 184 216 if (!password) return; 185 - if (password.length < 8) { showFieldStatus(this, 'error', 'At least 8 characters'); return; } 217 + if (password.length < 8) { showFieldStatus(this, 'error', 'at least 8 characters'); return; } 186 218 try { 187 219 const resp = await fetch('/init/password', { 188 220 method: 'POST', headers: {'Content-Type': 'application/json'}, ··· 224 256 }); 225 257 const data = await resp.json(); 226 258 if (data.success) { 227 - if (data.validation && data.validation.valid) showFieldStatus(this, 'saved', 'Connected'); 259 + if (data.validation && data.validation.valid) showFieldStatus(this, 'saved', 'connected'); 228 260 else showFieldStatus(this, 'error', data.validation?.error || 'Key saved, validation failed'); 229 261 } else showFieldStatus(this, 'error', data.error); 230 262 } catch (err) { showFieldStatus(this, 'error', 'Failed to save'); } ··· 240 272 try { 241 273 const resp = await fetch('/init/observers'); 242 274 const data = await resp.json(); 275 + const emptyEl = document.getElementById('observer-empty'); 243 276 if (data.length > 0) { 244 - document.getElementById('observer-status').style.display = 'block'; 277 + if (emptyEl) emptyEl.style.display = 'none'; 245 278 const now = Date.now(); 246 279 document.getElementById('observer-list').innerHTML = data.map(r => { 247 280 const connected = r.last_seen && (now - r.last_seen) < 120000; ··· 251 284 label + '</span><span class="observer-state">' + 252 285 (connected ? 'connected' : 'disconnected') + '</span></div>'; 253 286 }).join(''); 287 + } else { 288 + if (emptyEl) emptyEl.style.display = 'block'; 254 289 } 255 290 } catch (err) {} 256 291 }