From d886e4a6696e529dfc05b7dbdf9f93b28154ddbd Mon Sep 17 00:00:00 2001 From: Sagnik Date: Sun, 19 Apr 2026 17:07:12 +0530 Subject: [PATCH] fix: Applied fix for name, the oracle team sharing, sentinel client list visibility --- .gitignore | 4 + app/dist/index.html | 4 +- app/src/App.tsx | 54 ++++---- app/src/app/oracle/page.tsx | 4 +- app/src/components/layout/LoginScreen.tsx | 27 ++-- app/src/components/modules/Settings.tsx | 100 ++++++++++++-- app/src/lib/velocityPlatformClient.ts | 72 ++++++++++ app/src/oracle/components/ShareModal.tsx | 154 ++++++++++++++++------ app/src/oracle/hooks/useOraclePage.ts | 4 +- app/src/oracle/lib/oracleApiClient.ts | 9 +- app/src/types/index.ts | 2 + app/vite.config.ts | 8 +- backend/auth/dependencies.py | 12 +- backend/main.py | 122 ++++++++++++++++- docs/LOCAL_DEV_SNAPSHOT.md | 143 ++++++++++++++++++++ scripts/export_velocity_local_bundle.sh | 122 +++++++++++++++++ scripts/import_velocity_local_bundle.ps1 | 140 ++++++++++++++++++++ scripts/package_velocity_local_bundle.ps1 | 25 ++++ scripts/start_velocity_backend_local.ps1 | 28 ++++ scripts/start_velocity_frontend_local.ps1 | 15 +++ 20 files changed, 940 insertions(+), 109 deletions(-) create mode 100644 docs/LOCAL_DEV_SNAPSHOT.md create mode 100644 scripts/export_velocity_local_bundle.sh create mode 100644 scripts/import_velocity_local_bundle.ps1 create mode 100644 scripts/package_velocity_local_bundle.ps1 create mode 100644 scripts/start_velocity_backend_local.ps1 create mode 100644 scripts/start_velocity_frontend_local.ps1 diff --git a/.gitignore b/.gitignore index 473d768d..825c8201 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,10 @@ docker-compose.override.yml *.pem *.mp4 *.zip +.local-dev/ +runtime-snapshots/ +local-dev-bundles/ +scripts/tmp/ models/ comfy_engine/test_outputs/ diff --git a/app/dist/index.html b/app/dist/index.html index 4dcd52d9..80f38fd5 100644 --- a/app/dist/index.html +++ b/app/dist/index.html @@ -4,8 +4,8 @@ Velocity WebOS - - + +
diff --git a/app/src/App.tsx b/app/src/App.tsx index 7097735e..0711ed61 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -21,7 +21,7 @@ import { getVelocityToken, isAdminRole, normalizeVelocityRole, - type VelocityUserProfile, + resolveVelocityFirstName, } from '@/lib/velocityPlatformClient'; import { @@ -52,8 +52,8 @@ export const MODULE_ROUTES: Array<{ { id: 'sentinel', path: '/sentinel', title: 'The Sentinel', component: Sentinel }, { id: 'inventory', path: '/inventory', title: 'Inventory', component: Inventory }, { id: 'catalyst', path: '/catalyst', title: 'The Catalyst', component: Catalyst }, - { id: 'settings', path: '/settings', title: 'Settings', component: Settings }, { id: 'crm', path: '/crm', title: 'CRM', component: CRM }, + { id: 'settings', path: '/settings', title: 'Settings', component: Settings }, { id: 'admin', path: '/admin', title: 'Admin', component: AdminPage, adminOnly: true }, ]; @@ -98,7 +98,7 @@ function MainLayout() { const currentRoute = availableRoutes.find((r) => r.path === location.pathname); const pageTitle = currentRoute?.title ?? 'Velocity'; const roleLabel = formatRoleLabel(user?.role); - const userLabel = user?.name?.trim() || user?.id || 'Authenticated User'; + const userLabel = user?.name?.trim() || user?.fullName?.trim() || user?.email?.trim() || user?.id || 'Authenticated User'; const initials = userLabel .split(/\s+/) .filter(Boolean) @@ -187,7 +187,13 @@ function MainLayout() { Settings - logout()}> + { + clearVelocityToken(); + logout(); + }} + > Log out @@ -237,7 +243,7 @@ function MainLayout() { // ── Root App ────────────────────────────────────────────────────────────────── function App() { - const { isAuthenticated, login, logout } = useStore(); + const { isAuthenticated, login, logout, user } = useStore(); const [authBootstrapped, setAuthBootstrapped] = useState(false); useEffect(() => { @@ -257,9 +263,18 @@ function App() { void getVelocityMe() .then((me) => { if (cancelled) return; + const resolvedEmail = me.email?.trim() || user?.email?.trim() || undefined; + const resolvedFullName = me.full_name?.trim() || user?.fullName?.trim() || undefined; login({ id: me.user_id, - name: resolveVelocityDisplayName(me), + name: resolveVelocityFirstName({ + ...me, + email: resolvedEmail ?? null, + full_name: resolvedFullName ?? null, + }), + fullName: resolvedFullName, + email: resolvedEmail, + avatar: me.avatar_url?.trim() || user?.avatar?.trim() || undefined, role: normalizeVelocityRole(me.role), }); setAuthBootstrapped(true); @@ -286,9 +301,18 @@ function App() { void getVelocityMe() .then((me) => { if (cancelled) return; + const resolvedEmail = me.email?.trim() || user?.email?.trim() || undefined; + const resolvedFullName = me.full_name?.trim() || user?.fullName?.trim() || undefined; login({ id: me.user_id, - name: resolveVelocityDisplayName(me), + name: resolveVelocityFirstName({ + ...me, + email: resolvedEmail ?? null, + full_name: resolvedFullName ?? null, + }), + fullName: resolvedFullName, + email: resolvedEmail, + avatar: me.avatar_url?.trim() || user?.avatar?.trim() || undefined, role: normalizeVelocityRole(me.role), }); }) @@ -301,7 +325,7 @@ function App() { return () => { cancelled = true; }; - }, [authBootstrapped, isAuthenticated, login, logout]); + }, [authBootstrapped, isAuthenticated, login, logout, user]); if (!authBootstrapped) { return ( @@ -363,17 +387,3 @@ function formatRoleLabel(role: string | undefined) { .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); } - -function resolveVelocityDisplayName(profile: VelocityUserProfile) { - const fullName = profile.full_name?.trim(); - if (fullName) { - return fullName; - } - - const email = profile.email?.trim(); - if (email) { - return email; - } - - return profile.user_id; -} diff --git a/app/src/app/oracle/page.tsx b/app/src/app/oracle/page.tsx index 2a19d5bc..32635197 100644 --- a/app/src/app/oracle/page.tsx +++ b/app/src/app/oracle/page.tsx @@ -135,10 +135,10 @@ export default function OraclePage() { // ── Share handler ─────────────────────────────────────────────────────────── - const handleShare = useCallback(async (params: { recipientEmail: string; visibility: 'private' | 'team'; message: string; sourceRevision: number }) => { + const handleShare = useCallback(async (params: { recipientUserId: string; visibility: 'private' | 'team'; message: string; sourceRevision: number }) => { if (!page) return; await createFork(page.pageId, { - recipientUserId: params.recipientEmail, + recipientUserId: params.recipientUserId, sourceRevision: params.sourceRevision, visibility: params.visibility, message: params.message, diff --git a/app/src/components/layout/LoginScreen.tsx b/app/src/components/layout/LoginScreen.tsx index c49d7453..518666b4 100644 --- a/app/src/components/layout/LoginScreen.tsx +++ b/app/src/components/layout/LoginScreen.tsx @@ -6,7 +6,7 @@ import { clearVelocityToken, loginVelocity, normalizeVelocityRole, - type VelocityUserProfile, + resolveVelocityFirstName, } from '@/lib/velocityPlatformClient'; export function LoginScreen() { @@ -22,9 +22,18 @@ export function LoginScreen() { setError(''); try { const me = await loginVelocity(email.trim(), password); + const fallbackEmail = me.email?.trim() || email.trim(); + const fallbackFullName = me.full_name?.trim() || undefined; login({ id: me.user_id, - name: resolveVelocityDisplayName(me), + name: resolveVelocityFirstName({ + ...me, + email: fallbackEmail || null, + full_name: fallbackFullName || null, + }), + fullName: fallbackFullName, + email: fallbackEmail || undefined, + avatar: me.avatar_url?.trim() || undefined, role: normalizeVelocityRole(me.role), }); } catch (err) { @@ -172,17 +181,3 @@ export function LoginScreen() { ); } - -function resolveVelocityDisplayName(profile: VelocityUserProfile) { - const fullName = profile.full_name?.trim(); - if (fullName) { - return fullName; - } - - const email = profile.email?.trim(); - if (email) { - return email; - } - - return profile.user_id; -} diff --git a/app/src/components/modules/Settings.tsx b/app/src/components/modules/Settings.tsx index ea3c92da..0e0f1171 100644 --- a/app/src/components/modules/Settings.tsx +++ b/app/src/components/modules/Settings.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState, type ChangeEvent } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { User, @@ -15,13 +15,20 @@ import { Check, ChevronDown, LogOut, + Pencil, type LucideIcon, } from 'lucide-react'; import { useStore } from '@/store/useStore'; import { useCurrency, CURRENCY_OPTIONS } from '@/store/useCurrencyStore'; import type { CurrencyCode } from '@/store/useCurrencyStore'; import { API_URL } from '@/lib/api'; -import { clearVelocityToken, getVelocityToken, normalizeVelocityRole } from '@/lib/velocityPlatformClient'; +import { + clearVelocityToken, + getVelocityToken, + normalizeVelocityRole, + resolveVelocityFirstName, + uploadVelocityAvatar, +} from '@/lib/velocityPlatformClient'; // ── Design tokens (matching inventory glassmorphism) ───────────────────────── const GLASS = { @@ -361,35 +368,104 @@ function CompanionSurfacesCard() { // ── Profile ────────────────────────────────────────────────────────────────── function ProfileSettings() { - const { user } = useStore(); - const initials = user?.name.split(' ').map((n) => n[0]).join('') ?? 'AU'; + const { user, login } = useStore(); + const fileInputRef = useRef(null); + const [avatarError, setAvatarError] = useState(null); + const [isUploadingAvatar, setIsUploadingAvatar] = useState(false); + const displayName = user?.fullName?.trim() || user?.name?.trim() || user?.email?.trim() || user?.id || 'Authenticated User'; + const firstName = resolveVelocityFirstName({ + user_id: user?.id ?? '', + full_name: user?.fullName ?? user?.name ?? null, + email: user?.email ?? null, + }); + const initials = (user?.fullName || user?.name || user?.email || 'AU') + .split(/[\s@._-]+/) + .filter(Boolean) + .slice(0, 2) + .map((n) => n[0]?.toUpperCase() ?? '') + .join('') || 'AU'; const roleLabel = normalizeVelocityRole(user?.role) .toLowerCase() .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' ') || 'Authenticated User'; + const handleAvatarUpload = async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file || !user) { + return; + } + + setAvatarError(null); + setIsUploadingAvatar(true); + try { + const { avatar_url } = await uploadVelocityAvatar(file); + login({ + ...user, + avatar: avatar_url, + }); + } catch (error) { + setAvatarError(error instanceof Error ? error.message : 'Failed to upload profile picture.'); + } finally { + setIsUploadingAvatar(false); + if (event.target) { + event.target.value = ''; + } + } + }; + return (
-
- {initials} +
+ {user?.avatar ? ( + {displayName} + ) : ( +
+ {initials} +
+ )} +
-
-

{user?.name}

+
+

{displayName}

{roleLabel}

+ {isUploadingAvatar && ( +

Uploading profile picture...

+ )}
+
+ {avatarError && ( +

{avatarError}

+ )}
- {user?.name ?? 'Unavailable'} + {firstName || 'Unavailable'} {user?.id ?? 'Unavailable'} diff --git a/app/src/lib/velocityPlatformClient.ts b/app/src/lib/velocityPlatformClient.ts index cf2605fc..60f5ba25 100644 --- a/app/src/lib/velocityPlatformClient.ts +++ b/app/src/lib/velocityPlatformClient.ts @@ -7,6 +7,15 @@ export interface VelocityUserProfile { role: string; full_name?: string | null; email?: string | null; + avatar_url?: string | null; +} + +export interface VelocityActiveUser { + user_id: string; + role: string; + full_name?: string | null; + email?: string | null; + avatar_url?: string | null; } export interface VelocityLoginResponse { @@ -189,6 +198,39 @@ export function normalizeVelocityRole(role: string | null | undefined): string { return (role ?? '').trim().toUpperCase(); } +export function resolveVelocityFullName(profile: Pick): string { + const fullName = profile.full_name?.trim(); + if (fullName) { + return fullName; + } + + const email = profile.email?.trim(); + if (email) { + return email; + } + + return profile.user_id; +} + +export function resolveVelocityFirstName(profile: Pick): string { + const fullName = profile.full_name?.trim(); + if (fullName) { + return fullName.split(/\s+/)[0] ?? fullName; + } + + const email = profile.email?.trim(); + if (email) { + const local = email.split('@')[0]?.trim() ?? ''; + if (local) { + const normalized = local.replace(/[._-]+/g, ' ').trim(); + const firstToken = normalized.split(/\s+/)[0] ?? normalized; + return firstToken.charAt(0).toUpperCase() + firstToken.slice(1); + } + } + + return profile.user_id; +} + export function isAdminRole(role: string | null | undefined): boolean { const normalized = normalizeVelocityRole(role); return normalized === 'ADMIN' || normalized === 'SUPERADMIN'; @@ -209,6 +251,36 @@ export async function getVelocityMe(): Promise { }); } +export async function listVelocityUsers(): Promise { + return platformFetch('/api/auth/users', { + method: 'GET', + }); +} + +export async function uploadVelocityAvatar(file: File): Promise<{ avatar_url: string }> { + const form = new FormData(); + form.append('file', file); + + const response = await fetch(`${API_URL}/api/auth/profile/avatar`, { + method: 'POST', + headers: buildHeaders(undefined, false), + body: form, + }); + + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error( + typeof body?.detail === 'string' + ? body.detail + : typeof body?.message === 'string' + ? body.message + : `Request failed: ${response.status}`, + ); + } + + return response.json() as Promise<{ avatar_url: string }>; +} + export async function getAdminHealth(): Promise { return platformFetch('/api/admin-surface/health'); } diff --git a/app/src/oracle/components/ShareModal.tsx b/app/src/oracle/components/ShareModal.tsx index d95d4cbe..8dd3beb0 100644 --- a/app/src/oracle/components/ShareModal.tsx +++ b/app/src/oracle/components/ShareModal.tsx @@ -1,51 +1,99 @@ -/** - * ShareModal — Fork-based sharing workflow. - * Explains the direct_fork_only semantics (recipient gets an editable copy, - * not live edit access to owner's canvas). - */ -import { useState, useEffect } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Share2, GitFork, Lock, Users, MessageSquare, ChevronDown } from 'lucide-react'; import { Button } from '@/components/ui/button'; import type { CanvasPage } from '../types/canvas'; +import { listVelocityUsers, type VelocityActiveUser } from '@/lib/velocityPlatformClient'; interface ShareModalProps { page: CanvasPage | null; isOpen: boolean; onClose: () => void; onShare: (params: { - recipientEmail: string; + recipientUserId: string; visibility: 'private' | 'team'; message: string; sourceRevision: number; }) => Promise; } -const TEAM_MEMBERS = [ - { id: 'u2', name: 'Elena Rostova', email: 'elena@binghatti.ae', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=80&q=80', role: 'Senior Broker' }, - { id: 'u3', name: 'Priya Sharma', email: 'priya@binghatti.ae', avatar: 'https://images.unsplash.com/photo-1554151228-14d9def656e4?auto=format&fit=crop&w=80&q=80', role: 'Senior Broker' }, - { id: 'u4', name: 'Carlos Mendez', email: 'carlos@binghatti.ae', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=80&q=80', role: 'Broker' }, - { id: 'u5', name: 'Ravi Kapoor', email: 'ravi@binghatti.ae', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=80&q=80', role: 'Broker' }, -]; +function getDisplayName(member: VelocityActiveUser): string { + return member.full_name?.trim() || member.email?.trim() || member.user_id; +} + +function getRoleLabel(member: VelocityActiveUser): string { + return member.role + .toLowerCase() + .split('_') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); +} + +function getInitials(member: VelocityActiveUser): string { + const basis = getDisplayName(member); + return basis + .split(/[\s@._-]+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part.charAt(0).toUpperCase()) + .join('') || 'U'; +} export function ShareModal({ page, isOpen, onClose, onShare }: ShareModalProps) { const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - - const [recipient, setRecipient] = useState<{ id: string; name: string; email: string } | null>(null); + const [teamMembers, setTeamMembers] = useState([]); + const [loadingMembers, setLoadingMembers] = useState(false); + const [membersError, setMembersError] = useState(null); + const [recipient, setRecipient] = useState(null); const [visibility, setVisibility] = useState<'private' | 'team'>('private'); const [message, setMessage] = useState(''); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(false); const [memberDropOpen, setMemberDropOpen] = useState(false); + useEffect(() => setMounted(true), []); + + useEffect(() => { + if (!isOpen) { + setMemberDropOpen(false); + return; + } + + let cancelled = false; + setLoadingMembers(true); + setMembersError(null); + + void listVelocityUsers() + .then((users) => { + if (cancelled) return; + setTeamMembers(users); + }) + .catch((error) => { + if (cancelled) return; + setMembersError(error instanceof Error ? error.message : 'Failed to load team members.'); + setTeamMembers([]); + }) + .finally(() => { + if (!cancelled) setLoadingMembers(false); + }); + + return () => { + cancelled = true; + }; + }, [isOpen]); + + const selectedRecipientLabel = useMemo( + () => (recipient ? getDisplayName(recipient) : 'Select verified teammate...'), + [recipient], + ); + const handleShare = async () => { if (!recipient || !page) return; setSubmitting(true); try { await onShare({ - recipientEmail: recipient.email, + recipientUserId: recipient.user_id, visibility, message, sourceRevision: page.headRevision, @@ -56,9 +104,9 @@ export function ShareModal({ page, isOpen, onClose, onShare }: ShareModalProps) onClose(); setRecipient(null); setMessage(''); - }, 2000); + }, 1800); } catch { - // stay open on error + // keep modal open and let caller surface the error upstream } finally { setSubmitting(false); } @@ -68,7 +116,6 @@ export function ShareModal({ page, isOpen, onClose, onShare }: ShareModalProps) {isOpen && ( <> - {/* Backdrop */} - {/* Header */}
@@ -113,16 +159,14 @@ export function ShareModal({ page, isOpen, onClose, onShare }: ShareModalProps)
- {/* Fork explanation */}

- The recipient gets a fork — an editable copy - of this canvas at revision {page?.headRevision}. They can build on it and open a merge - request to propose their changes back. + The recipient gets a fork of this canvas at the selected revision. + They can edit their copy and later open a merge request back into the source branch.

@@ -131,17 +175,16 @@ export function ShareModal({ page, isOpen, onClose, onShare }: ShareModalProps)
-

Fork created successfully!

-

{recipient?.name} can now access their copy.

+

Fork created successfully.

+

{recipient ? getDisplayName(recipient) : 'Recipient'} can access the shared copy.

) : (
- {/* Recipient picker */}
+ {memberDropOpen && ( - {TEAM_MEMBERS.map((m) => ( + {loadingMembers && ( +
Loading verified accounts...
+ )} + {!loadingMembers && membersError && ( +
{membersError}
+ )} + {!loadingMembers && !membersError && teamMembers.length === 0 && ( +
No verified users available.
+ )} + {!loadingMembers && !membersError && teamMembers.map((member) => ( ))} @@ -183,7 +254,6 @@ export function ShareModal({ page, isOpen, onClose, onShare }: ShareModalProps)
- {/* Visibility */}
@@ -210,7 +280,6 @@ export function ShareModal({ page, isOpen, onClose, onShare }: ShareModalProps)
- {/* Message */}