OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking commits / 71eecd2

Add profile page with name/email/phone, auto-claim provider on first sign-in
- Profile page: save display name, email, phone via PUT /api/profile - Auto-claim: first sign-in user automatically becomes owner of seeded provider - Dashboard detects no-provider state and shows helpful message Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
71eecd2aec4ce347c9e4749672f3ee22b88d09df
by plankatron <fleeky@gmail.com> · 2026-03-22T18:36:10.000Z

Changed files (2) +136 -13

lib/routes/dashboard.js +64 -0
……@@ -8,6 +8,36 @@ import { requireAuth } from './auth-helpers.js'
88export function registerDashboardRoutes (router, server) {
99 // ---- API Endpoints ----
1010
11 // Get provider for current user (or claim unclaimed)
12 router.get('/api/dashboard/provider', async (req, res) => {
13 const auth = await requireAuth(req, server, 'read')
14 let providerId = await getProviderIdForUser(server, auth.userId)
15
16 if (!providerId) {
17 // Check if there are any unclaimed providers (userId = 'admin' or missing)
18 for await (const entry of server.db.createReadStream({
19 gte: 'provider:',
20 lte: 'provider:~'
21 })) {
22 const p = entry.value
23 if (p.userId === 'admin' || !p.userId) {
24 // Claim this provider
25 p.userId = auth.userId
26 await server.db.put(`provider:${p.id}`, p)
27 providerId = p.id
28 break
29 }
30 }
31 }
32
33 if (!providerId) {
34 return json(res, { provider: null })
35 }
36
37 const providerEntry = await server.db.get(`provider:${providerId}`)
38 json(res, { provider: providerEntry ? providerEntry.value : null })
39 })
40
1141 // Today's schedule + summary
1242 router.get('/api/dashboard/today', async (req, res) => {
1343 const auth = await requireAuth(req, server, 'write')
……@@ -195,6 +225,40 @@ export function registerDashboardRoutes (router, server) {
195225 })
196226 })
197227
228 // Update profile (any user)
229 router.put('/api/profile', async (req, res) => {
230 const auth = await requireAuth(req, server, 'read')
231 const body = await readJson(req)
232 const { displayName, email, phone } = body
233
234 // Update user record in identity registry
235 const userEntry = await server.db.get(`user:${auth.userId}`)
236 const user = userEntry ? userEntry.value : { userId: auth.userId, createdAt: Date.now() }
237
238 if (displayName !== undefined) user.displayName = displayName
239 if (email !== undefined) user.email = email
240 if (phone !== undefined) user.phone = phone
241 user.updatedAt = Date.now()
242
243 await server.db.put(`user:${auth.userId}`, user)
244
245 // Also update username in identity registry if available
246 if (displayName && server.authManager.identityRegistry) {
247 try {
248 await server.authManager.identityRegistry.setUsername(auth.userId, displayName)
249 } catch {}
250 }
251
252 json(res, user)
253 })
254
255 // Get profile
256 router.get('/api/profile', async (req, res) => {
257 const auth = await requireAuth(req, server, 'read')
258 const userEntry = await server.db.get(`user:${auth.userId}`)
259 json(res, userEntry ? userEntry.value : { userId: auth.userId })
260 })
261
198262 // ---- HTML Pages ----
199263
200264 // Dashboard home
web/js/dashboard.js +72 -13
……@@ -29,6 +29,25 @@ async function initDashboard (view, params) {
2929 return
3030 }
3131
32 // Auto-claim unclaimed provider on first dashboard visit
33 if (view === 'home' || view === 'clients' || view === 'packages' || view === 'availability' || view === 'analytics' || view === 'settings') {
34 try {
35 const provData = await api('GET', '/api/dashboard/provider')
36 if (!provData.provider) {
37 content.innerHTML = `
38 <div class="card" style="text-align:center;padding:3rem;">
39 <h2>No Provider Profile</h2>
40 <p style="color:var(--text-secondary);margin:1rem 0;">You need a provider profile to use the dashboard.</p>
41 <p style="color:var(--text-secondary);">If you just signed in for the first time, refresh the page — your account may have been auto-linked.</p>
42 <button class="btn btn-accent" onclick="location.reload()" style="margin-top:1rem;">Refresh</button>
43 </div>`
44 return
45 }
46 } catch (err) {
47 // Non-blocking — continue anyway
48 }
49 }
50
3251 switch (view) {
3352 case 'home': return renderCalendar()
3453 case 'clients': return renderClients()
……@@ -648,24 +667,64 @@ async function renderSettings () {
648667
649668async function renderProfile () {
650669 const hasPk = HyperBookingIdentity.hasKeypair()
670
671 if (!hasPk) {
672 content.innerHTML = `
673 <div class="card" style="text-align:center;padding:2rem;">
674 <p>You are not signed in.</p>
675 <button class="btn btn-accent" onclick="signIn()" style="margin-top:1rem;">Create Identity</button>
676 </div>`
677 return
678 }
679
680 let profile = {}
681 try {
682 profile = await api('GET', '/api/profile')
683 } catch {}
684
651685 content.innerHTML = `
652686 <div class="card">
687 <h3 style="margin-bottom:1rem;">Your Profile</h3>
688 <form id="profile-form">
689 <label>Display Name</label>
690 <input type="text" id="prof-name" value="${esc(profile.displayName || '')}" placeholder="Your name">
691 <label>Email (for notifications)</label>
692 <input type="email" id="prof-email" value="${esc(profile.email || '')}" placeholder="you@email.com">
693 <label>Phone (for SMS reminders, optional)</label>
694 <input type="tel" id="prof-phone" value="${esc(profile.phone || '')}" placeholder="+1 555 123 4567">
695 <button type="submit" class="btn btn-accent">Save Profile</button>
696 </form>
697 </div>
698
699 <div class="card" style="margin-top:1rem;">
653700 <h3 style="margin-bottom:1rem;">Identity</h3>
654 ${hasPk ? `
655 <p style="font-size:0.85rem;color:var(--text-secondary);word-break:break-all;">
656 Public Key: ${HyperBookingIdentity.getPublicKey()}
657 </p>
658 <div style="margin-top:1rem;display:flex;gap:0.5rem;">
659 <button class="btn btn-secondary" onclick="exportKeypair()">Export Keypair</button>
660 <button class="btn btn-secondary" onclick="importKeypairPrompt()">Import Keypair</button>
661 <button class="btn btn-danger" onclick="signOut()">Sign Out</button>
662 </div>
663 ` : `
664 <p>You are not signed in.</p>
665 <button class="btn btn-accent" onclick="signIn()">Create Identity</button>
666 `}
701 <p style="font-size:0.85rem;color:var(--text-secondary);word-break:break-all;">
702 Public Key: ${HyperBookingIdentity.getPublicKey()}
703 </p>
704 <div style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
705 <button class="btn btn-secondary" onclick="exportKeypair()">Export Keypair</button>
706 <button class="btn btn-secondary" onclick="importKeypairPrompt()">Import Keypair</button>
707 <button class="btn btn-danger" onclick="signOut()">Sign Out</button>
708 </div>
667709 </div>
668710 `
711
712 document.getElementById('profile-form').addEventListener('submit', async (e) => {
713 e.preventDefault()
714 try {
715 await api('PUT', '/api/profile', {
716 displayName: document.getElementById('prof-name').value,
717 email: document.getElementById('prof-email').value,
718 phone: document.getElementById('prof-phone').value
719 })
720 showToast('Profile saved', 'success')
721 // Update nav display name
722 const nameEl = document.getElementById('user-name')
723 if (nameEl) nameEl.textContent = document.getElementById('prof-name').value
724 } catch (err) {
725 showToast(err.message, 'error')
726 }
727 })
669728}
670729
671730// ---- Portal ----