fix: Backend added along with missing pages
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled

This commit is contained in:
2026-05-01 14:42:42 +05:30
parent effd19531a
commit 70ef8578d0
60 changed files with 25045 additions and 30 deletions

26
webos/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,26 @@
module.exports = {
root: true,
env: {
browser: true,
es2022: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'react-hooks'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-unused-vars': 'off',
'no-useless-escape': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
ignorePatterns: ['dist', 'node_modules'],
};

2436
webos/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,19 +12,59 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-three/drei": "^10.0.0",
"@react-three/fiber": "^9.0.0",
"@tanstack/react-query": "^5.51.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^11.3.0",
"input-otp": "^1.4.2",
"lucide-react": "^1.14.0",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-day-picker": "^9.14.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.74.0",
"react-resizable-panels": "^4.10.0",
"react-router-dom": "^6.26.0",
"recharts": "^3.8.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"three": "0.168.0",
"vaul": "^1.1.2",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/three": "^0.168.0",

View File

@@ -29,7 +29,7 @@ export default function ShowroomMode() {
const navigate = useNavigate();
const [phase, setPhase] = useState<ShowroomPhase>('live');
const [elapsed, setElapsed] = useState(0); // seconds
const timerRef = useRef<ReturnType<typeof setInterval>>();
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const {
isShowroomActive,
@@ -43,12 +43,14 @@ export default function ShowroomMode() {
// Session timer
useEffect(() => {
timerRef.current = setInterval(() => setElapsed(s => s + 1), 1000);
return () => clearInterval(timerRef.current);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
// End session → summary
const handleEndSession = () => {
clearInterval(timerRef.current);
if (timerRef.current) clearInterval(timerRef.current);
setPhase('summary');
setShowroomActive(false);
};

View File

@@ -1,6 +1,6 @@
import { useState, useRef } from 'react';
import { motion } from 'framer-motion';
import { useConversations } from '../../../shared/hooks/useClient360';
import { useConversations } from '@/shared/hooks/useClient360';
import styles from './Conversations.module.css';
/**

View File

@@ -178,7 +178,7 @@ export default function PropertyEntity() {
// ── 3D Model component (R3F) ──────────────────────────────────
function PropertyModel({ url }: { url: string }) {
const { scene } = useGLTF(url);
const meshRef = useRef<any>();
const meshRef = useRef<any>(null);
useFrame((_, delta) => {
if (meshRef.current) {
@@ -191,7 +191,7 @@ function PropertyModel({ url }: { url: string }) {
// Placeholder building geometry when no GLB model available
function PlaceholderBuilding() {
const ref = useRef<any>();
const ref = useRef<any>(null);
useFrame((_, delta) => {
if (ref.current) ref.current.rotation.y += delta * 0.1;
});

View File

@@ -14,8 +14,8 @@ export function AdminGuard({ children }: { children: React.ReactNode }) {
return <Navigate to="/login" replace />;
}
const isAdmin =
user?.role === 'ADMIN' || user?.role === 'SALES_DIRECTOR';
const role = String(user?.role ?? '').trim().toUpperCase();
const isAdmin = role === 'ADMIN' || role === 'SALES_DIRECTOR' || role === 'SUPERADMIN';
if (!isAdmin) {
// Silent redirect — no error screen (security through obscurity)

View File

@@ -34,7 +34,8 @@ const PILLARS = [
export function NavRail() {
const { user } = useAuthStore();
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SALES_DIRECTOR';
const role = String(user?.role ?? '').trim().toUpperCase();
const isAdmin = role === 'ADMIN' || role === 'SALES_DIRECTOR' || role === 'SUPERADMIN';
return (
<nav className={styles.rail} aria-label="Main navigation">

View File

@@ -118,14 +118,20 @@ function ChartTooltipContent({
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
}: React.ComponentProps<"div"> & {
active?: boolean
payload?: any[]
label?: any
labelFormatter?: (value: any, payload: any[]) => React.ReactNode
labelClassName?: string
formatter?: (...args: any[]) => React.ReactNode
color?: string
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
@@ -258,11 +264,12 @@ function ChartLegendContent({
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
}: React.ComponentProps<"div"> & {
payload?: any[]
verticalAlign?: "top" | "bottom" | "middle"
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {

View File

@@ -0,0 +1,68 @@
/**
* curveGenerator.ts
* Converts an array of {x, y} data points into a smooth SVG cubic Bezier path string.
* Uses a Catmull-Rom → Bezier conversion for monotone-Y curves.
*/
export interface Point {
x: number;
y: number;
}
/**
* Generates a smooth SVG path string from an array of points using
* cubic Bezier curves (Catmull-Rom spline converted to Bezier).
*
* @param points Array of {x, y} pixel coordinates (already mapped from data).
* @param tension Controls curve tightness. 0 = straight lines, 1 = very curvy. Default 0.4.
* @returns SVG path `d` attribute string.
*/
export function generateSmoothPath(points: Point[], tension = 0.4): string {
if (points.length === 0) return '';
if (points.length === 1) return `M ${points[0].x},${points[0].y}`;
const d: string[] = [`M ${points[0].x},${points[0].y}`];
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[Math.max(i - 1, 0)];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = points[Math.min(i + 2, points.length - 1)];
// Catmull-Rom control points
const cp1x = p1.x + ((p2.x - p0.x) / 6) * tension * 3;
const cp1y = p1.y + ((p2.y - p0.y) / 6) * tension * 3;
const cp2x = p2.x - ((p3.x - p1.x) / 6) * tension * 3;
const cp2y = p2.y - ((p3.y - p1.y) / 6) * tension * 3;
d.push(`C ${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(2)},${cp2y.toFixed(2)} ${p2.x},${p2.y}`);
}
return d.join(' ');
}
/**
* Maps a sentiment score (0100) to a pixel X coordinate within a container.
*
* @param score Sentiment score 0100.
* @param width Container pixel width.
* @param padding Left/right padding in pixels. Default 40.
*/
export function sentimentToX(score: number, width: number, padding = 40): number {
const usable = width - padding * 2;
return padding + (score / 100) * usable;
}
/**
* Maps an event index to a pixel Y coordinate within a container.
*
* @param index Event index (0 = top).
* @param total Total number of events.
* @param height Container pixel height.
* @param padding Top/bottom padding in pixels. Default 40.
*/
export function indexToY(index: number, total: number, height: number, padding = 40): number {
if (total <= 1) return height / 2;
const usable = height - padding * 2;
return padding + (index / (total - 1)) * usable;
}

View File

@@ -8,7 +8,7 @@ interface User {
id: string;
name: string;
email: string;
role: 'SALES_BROKER' | 'SALES_DIRECTOR' | 'ADMIN';
role: 'SALES_BROKER' | 'SALES_DIRECTOR' | 'ADMIN' | 'SUPERADMIN' | string;
avatarUrl?: string;
}
@@ -37,7 +37,11 @@ export const useAuthStore = create<AuthStore>()(
}),
{
name: 'velocity-auth',
partialize: (state) => ({ token: state.token, user: state.user }),
partialize: (state) => ({
token: state.token,
user: state.user,
isAuthenticated: Boolean(state.token && state.user),
}),
}
),
{ name: 'AuthStore' }

6
webos/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>;
export default classes;
}

View File

@@ -2,6 +2,7 @@
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -15,9 +16,15 @@
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@/components/ui/*": ["./src/shared/ui/*"],
"@/hooks/*": ["./src/shared/hooks/*"],
"@/lib/*": ["./src/shared/lib/*"],
"@/store/*": ["./src/store/*"],
"@/types": ["./src/shared/types/index.ts"],
"@/types/*": ["./src/shared/types/*"],
"@/utils/*": ["./src/shared/utils/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src"]
}

View File

@@ -1,12 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"lib": ["ES2023", "DOM"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"composite": true,
"strict": true,
"noEmit": true
"noEmit": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
}

View File

@@ -13,6 +13,12 @@ export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@/components/ui': path.resolve(__dirname, './src/shared/ui'),
'@/hooks': path.resolve(__dirname, './src/shared/hooks'),
'@/lib': path.resolve(__dirname, './src/shared/lib'),
'@/store': path.resolve(__dirname, './src/store'),
'@/types': path.resolve(__dirname, './src/shared/types'),
'@/utils': path.resolve(__dirname, './src/shared/utils'),
},
},