feat: persona onboarding — invite tokens, self-service setup, persona creation, switcher
New user flow:
1. Admin: python manage_passwords.py invite <username> → generates URL
2. User visits /setup/<token> → sets own password → logged in
3. User redirected to /setup/persona → fills name/emoji/description
4. persona_template.py generates all starter files → lands at /{user}/{persona}
Multiple personas:
- Header persona name is now a clickable dropdown listing all personas
- "New persona" link at bottom → /setup/persona (available to logged-in users)
- /api/personas endpoint returns persona list for current session user
New files:
- persona_template.py: generates IDENTITY/SOUL/PROTOCOLS/USER/HELP.md + data files
- routers/onboarding.py: /setup/{token}, /setup/persona GET+POST
- static/setup.html: two-step form (password → persona), emoji picker, mobile-friendly
Updated:
- auth_utils.py: create_invite(), validate_invite(), consume_invite()
- manage_passwords.py: invite command with URL output
- auth_middleware.py: /setup/* prefix is public (invite tokens need no auth)
- routers/ui.py: /api/personas endpoint; post-login redirect if no personas
- static/app.js: persona switcher dropdown with navigation + Add persona link
- static/style.css: .persona-switcher, .persona-dropdown, mobile adjustments
Mobile: login/setup pages are card-centered with responsive padding;
dropdown avoids edge-clipping on narrow screens; logout button stays visible.
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,56 @@
|
||||
header .name { font-size: 1.1rem; font-weight: 600; color: var(--accent); }
|
||||
header .subtitle { font-size: 0.78rem; color: var(--muted); }
|
||||
|
||||
/* Persona switcher */
|
||||
.persona-switcher {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.persona-switcher:hover .name { text-decoration: underline dotted; }
|
||||
|
||||
.persona-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
min-width: 160px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||
z-index: 200;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.persona-dropdown.open { display: block; }
|
||||
|
||||
.persona-dropdown a {
|
||||
display: block;
|
||||
padding: 0.55rem 0.85rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.persona-dropdown a:hover { background: var(--border); }
|
||||
|
||||
.persona-dropdown a.active { color: var(--accent); font-weight: 600; }
|
||||
|
||||
.persona-dropdown .pd-divider {
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.persona-dropdown .pd-add {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.persona-dropdown .pd-add:hover { color: var(--text); }
|
||||
|
||||
.hdr-btn {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
@@ -1014,6 +1064,12 @@
|
||||
@media (max-width: 520px) {
|
||||
header { padding: 8px 12px; gap: 8px; }
|
||||
header .subtitle { display: none; }
|
||||
|
||||
/* Persona dropdown: avoid clipping off left edge on narrow screens */
|
||||
.persona-dropdown { left: 0; right: auto; min-width: 140px; }
|
||||
|
||||
/* Logout button: keep visible but compact */
|
||||
#logout-btn { padding: 5px 8px; font-size: 1rem; }
|
||||
#messages { padding: 12px; }
|
||||
|
||||
/* dvh adjusts as soft keyboard opens/closes */
|
||||
|
||||
Reference in New Issue
Block a user