From 86ad94f812d0748a9aa9d825bd2fa09cd6e1c4c1 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:28:59 -0800 Subject: [PATCH] fix: resolve API prefix double-url bugs and stabilize 3D viewer --- docs/SESSION-SUMMARY.md | 215 +++----------- frontend/src/lib/messagingApi.ts | 20 +- frontend/src/lib/uploadApi.ts | 7 +- frontend/src/pages/Facility3DViewerPage.tsx | 311 +++++--------------- 4 files changed, 128 insertions(+), 425 deletions(-) diff --git a/docs/SESSION-SUMMARY.md b/docs/SESSION-SUMMARY.md index 8e44c5e..0b16592 100644 --- a/docs/SESSION-SUMMARY.md +++ b/docs/SESSION-SUMMARY.md @@ -19,194 +19,51 @@ - Password hashing with bcrypt - JWT token generation (access 15m + refresh 7d) -- Updated login endpoint with proper tokens -- Added refresh & logout endpoints -- Created 4 test users (all roles) -- **Time**: 1 hour +- Updated login endpoint with prop## Features Modified, Added, or Removed: -### Sprint 2.5: Mobile-First Foundation ✅ +- **`backend/src/controllers/walkthrough.controller.ts` (Modified):** + - **Added `getTodaysWalkthrough`:** Logic to fetch the walkthrough created today (00:00 - 23:59). + - **Improved Error Handling:** Added checks for user existence before creating walkthroughs to prevent 500 errors with stale tokens (Foreign Key violations). +- **`backend/src/routes/walkthrough.routes.ts` (Modified):** + - Added `GET /today` endpoint. +- **`backend/src/routes/layout.routes.ts` (Modified):** + - **Added 3D Data Endpoint:** `GET /floors/:id/3d` returns optimized hierarchy (Floor -> Rooms -> Sections -> Positions) for 3D rendering. +- **`frontend/src/lib/layoutApi.ts` (Modified):** + - Added `getFloor3D` method and `Floor3DData` types. + - **Fixed API Prefix:** Removed double `/api` prefix from all calls. +- **`frontend/src/lib/{auditApi, documentsApi, messagingApi, uploadApi}.ts` (Modified):** + - **Fixed API Prefix:** Removed double `/api` prefix from all calls. +- **`frontend/src/pages/Facility3DViewerPage.tsx` (New)::** + - **Interactive 3D Scene:** Uses React Three Fiber to render the facility. + - **Features:** Orbit controls, zoom/pan, room color coding, plant positions, floor selection, stats panel. +- **`frontend/src/pages/DocumentsPage.tsx` (Modified):** + - Fixed TypeScript validation error for dynamic Lucide icons. (Imported `LucideIcon` type). +- **`docs/SPEC-KIT-AUDIT.md` (New):** + - Comprehensive audit of all 20+ specs vs. implementation code and demo data. -- Mobile-first Tailwind config -- Touch-friendly base styles (44px+ targets) -- 777 Wolfpack branding integration -- Mobile-optimized LoginPage -- Splash screen component -- **Time**: 45 minutes +## Dependencies and APIs ---- +- **New Endpoint:** `GET /api/layout/floors/:id/3d` (Optimized 3D data). +- **New Endpoint:** `GET /api/walkthroughs/today` (Check status). +- **Fix:** Removed `/api` prefix from front-end API libraries (was causing `/api/api/...` 404s). -## 🏗️ **What's Built** +## Design Decisions -### Backend (Fully Functional) +- **3D Viewer Strategy:** Implemented a read-only 3D viewer first (`Facility3DViewerPage`) separate from the complex `LayoutDesigner`. This fulfills the urgent need to "visualize plant locations" without blocking on a complex editor refactor. +- **Stale Token Handling:** explicitly check for user existence in `createWalkthrough` to return 401 instead of 500 when the DB is re-seeded but client has old token. -- ✅ Fastify server with TypeScript -- ✅ PostgreSQL + Prisma ORM -- ✅ Bcrypt password hashing -- ✅ JWT authentication (access + refresh tokens) -- ✅ Auth endpoints: `/login`, `/refresh`, `/logout`, `/me` -- ✅ Seed script with hashed passwords -- ✅ Health check working +## Existing Blockers and Bugs -### Frontend (Mobile-First Foundation) +1. **Stale JWT Tokens:** Users may need to log out and back in after a DB re-seed (now handled gracefully with 401). +2. **Layout Designer Complexity:** The drag-and-drop designer is still complex; we are testing if the 3D viewer + simplified input forms is a better direction. -- ✅ Vite + React + TypeScript -- ✅ Tailwind CSS with mobile-first breakpoints -- ✅ Touch-friendly base styles -- ✅ 777 Wolfpack branding -- ✅ Responsive LoginPage -- ✅ Splash screen component -- ✅ Dark mode support +## Next Steps to Solve the Problem ---- - -## 🔐 **Test Users (All Ready)** - -| Email | Password | Role | Rate | -|-------|----------|------|------| -| | password123 | OWNER | $50/hr | -| | password123 | MANAGER | $35/hr | -| | password123 | GROWER | $30/hr | -| | password123 | STAFF | $20/hr | - -All passwords are **bcrypt hashed** in the database. - ---- - -## 📱 **Mobile-First Features** - -### Responsive Breakpoints - -``` -xs: 375px (Large phones) -sm: 640px (Small tablets portrait) -md: 768px (Tablets portrait - PRIMARY TARGET) -lg: 1024px (Tablets landscape) -xl: 1280px (Desktop) -2xl: 1536px (Large desktop) -``` - -### Touch Optimizations - -- **Minimum tap targets**: 44px (buttons are 56px) -- **Font size**: 16px minimum (prevents iOS zoom) -- **Input height**: 44px minimum -- **Smooth scrolling**: Enabled -- **Tap highlights**: Removed -- **Touch manipulation**: Optimized - -### 777 Wolfpack Branding - -- Logo: `/frontend/public/assets/logo-777-wolfpack.jpg` -- Displayed on login page -- "777 Wolfpack Edition" subtitle -- Team name in footer -- Blue/slate color scheme - ---- - -## ⏭️ **What's Next** - -### Immediate Priorities - -#### 1. Complete Mobile-First UI (1-1.5 hours) - -- [ ] Mobile navigation (bottom nav mobile, side nav tablet+) -- [ ] Responsive Dashboard layout -- [ ] Mobile-optimized Rooms page -- [ ] Mobile-optimized Batches page -- [ ] Touch-friendly Timeclock page - -#### 2. Finish Sprint 2 Auth (1 hour) - -- [ ] Auth middleware (`authenticate`, `authorize`) -- [ ] Protect all API routes -- [ ] RBAC enforcement -- [ ] Frontend token management - -#### 3. Deploy & Test (30 min) - -- [ ] Deploy to nexus-vector -- [ ] Re-seed database with new users -- [ ] Test on actual iPad/tablet -- [ ] Get 777 Wolfpack team feedback - ---- - -## 🚀 **Deployment Ready** - -### What Needs to Happen - -1. **Push to Forgejo** (when it's back up) -2. **Re-seed database** on nexus-vector with new users -3. **Rebuild containers** to get new frontend assets -4. **Test login** with new token format - -### Commands - -```bash -# On nexus-vector -cd /srv/containers/ca-grow-ops-manager -git pull origin main -docker compose build -docker compose exec backend npx prisma db seed -docker compose up -d -``` - ---- - -## 📊 **Progress Metrics** - -| Category | Progress | Status | -|----------|----------|--------| -| Backend Infrastructure | 100% | ✅ Complete | -| Authentication Core | 80% | 🟡 Needs middleware | -| Mobile-First UI | 30% | 🟡 In Progress | -| RBAC | 0% | ⏳ Planned | -| Testing | 0% | ⏳ Planned | - -**Overall Phase 1**: ~60% Complete - ---- - -## 🎯 **Key Achievements** - -1. **Solid Backend Foundation** - - Production-ready auth with bcrypt + JWT - - Proper token management (access + refresh) - - Clean TypeScript architecture - -2. **Mobile-First Approach** - - Touch-optimized from the ground up - - Tablet-first for cultivation floor - - 777 Wolfpack branding integrated - -3. **Clean Documentation** - - Sprint plans documented - - Credentials documented - - Progress tracked - -4. **Short, Focused Sprints** - - Each sprint ~30-60 minutes - - Clear deliverables - - Thorough documentation - ---- - -## 💡 **Lessons Learned** - -1. **Mobile-first is critical** for cultivation floor apps -2. **Touch targets matter** - 44px minimum is non-negotiable -3. **Branding early** helps team buy-in (777 Wolfpack) -4. **Short sprints work** - easier to maintain context across sessions -5. **Documentation is key** - helps recover from terminated sessions - ---- - -## 🙏 **Thank You!** - -Great collaboration! The 777 Wolfpack team is going to have a solid, mobile-optimized cultivation management tool. - -**Next session**: Continue mobile-first refactor or finish auth middleware - your choice! +1. **Verify 3D Viewer:** User to test `https://777wolfpack.runfoo.run/facility-3d`. +2. **Verify Walkthrough:** Confirm "Already Completed" status and that "Oops" error is resolved (via re-login). +3. **Simplify Layout Tools:** Based on feedback, potentially replace the Drag-and-Drop designer with a "Wizard" style form that populates the 3D view. +4. **METRC Integration:** Begin Phase 2 (Metrc) based on the audit findings. +sh auth middleware - your choice! --- diff --git a/frontend/src/lib/messagingApi.ts b/frontend/src/lib/messagingApi.ts index 08f44ed..dfd0e35 100644 --- a/frontend/src/lib/messagingApi.ts +++ b/frontend/src/lib/messagingApi.ts @@ -50,27 +50,21 @@ export const messagingApi = { }, async markRead(id: string): Promise { - await api.post(`/api/messaging/announcements/${id}/read`); + await api.post(`/messaging/announcements/${id}/read`); }, async acknowledge(id: string): Promise<{ success: boolean; acknowledgedAt: string }> { - const response = await api.post(`/api/messaging/announcements/${id}/acknowledge`); + const response = await api.post(`/messaging/announcements/${id}/acknowledge`); return response.data; }, - async getAcknowledgements(id: string): Promise<{ - announcement: { id: string; title: string }; - requiresAck: boolean; - totalUsers: number; - acknowledged: number; - pending: { id: string; name: string; email: string }[]; - }> { - const response = await api.get(`/api/messaging/announcements/${id}/acks`); + async getAcknowledgements(id: string): Promise { + const response = await api.get(`/messaging/announcements/${id}/acks`); return response.data; }, // Shift Notes - async getShiftNotes(params?: { roomId?: string; batchId?: string; limit?: number }): Promise { + async getShiftNotes(params?: { date?: string; limit?: number; roomId?: string; batchId?: string }): Promise { const response = await api.get('/messaging/shift-notes', { params }); return response.data; }, @@ -86,7 +80,9 @@ export const messagingApi = { return response.data; }, + async markShiftNoteRead(id: string): Promise { - await api.post(`/api/messaging/shift-notes/${id}/read`); + await api.post(`/messaging/shift-notes/${id}/read`); } }; + diff --git a/frontend/src/lib/uploadApi.ts b/frontend/src/lib/uploadApi.ts index 17a6547..8496ef9 100644 --- a/frontend/src/lib/uploadApi.ts +++ b/frontend/src/lib/uploadApi.ts @@ -52,7 +52,7 @@ export async function uploadPhoto( const formData = new FormData(); formData.append('file', compressed.file, file.name); - const response = await api.post('/api/upload/photo', formData, { + const response = await api.post('/upload/photo', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { if (progressEvent.total && onProgress) { @@ -82,7 +82,7 @@ export async function uploadPhotos( formData.append('files', photo.file, files[index].name); }); - const response = await api.post('/api/upload/photos', formData, { + const response = await api.post('/upload/photos', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { if (progressEvent.total && onProgress) { @@ -93,6 +93,7 @@ export async function uploadPhotos( } }); + return response.data; } @@ -116,7 +117,7 @@ export function getPhotoUrl(path: string, size: 'thumb' | 'medium' | 'full' = 'm * Delete a photo */ export async function deletePhoto(photoId: string): Promise { - await api.delete(`/api/upload/photo/${photoId}`); + await api.delete(`/upload/photo/${photoId}`); } /** diff --git a/frontend/src/pages/Facility3DViewerPage.tsx b/frontend/src/pages/Facility3DViewerPage.tsx index a8c36bf..32cef40 100644 --- a/frontend/src/pages/Facility3DViewerPage.tsx +++ b/frontend/src/pages/Facility3DViewerPage.tsx @@ -1,11 +1,11 @@ -import { useEffect, useState, useRef } from 'react'; -import { Canvas, useFrame, useThree } from '@react-three/fiber'; -import { OrbitControls, Text, PerspectiveCamera } from '@react-three/drei'; -import { Loader2, Grid3X3, ChevronDown, ZoomIn, ZoomOut, RotateCcw, Building } from 'lucide-react'; +import { useEffect, useState, useRef, Suspense } from 'react'; +import { Canvas, useThree } from '@react-three/fiber'; +import { OrbitControls, Text } from '@react-three/drei'; +import { Loader2, Grid, ChevronDown, Building, AlertTriangle } from 'lucide-react'; import { layoutApi, Floor3DData, Room3D, Section3D, Position3D } from '../lib/layoutApi'; import * as THREE from 'three'; -// Color mapping for room types +// Color mapping for room types - with fallback const ROOM_COLORS: Record = { VEG: '#22c55e', // Green FLOWER: '#a855f7', // Purple @@ -13,7 +13,8 @@ const ROOM_COLORS: Record = { DRY: '#f97316', // Orange CURE: '#92400e', // Brown CLONE: '#14b8a6', // Teal - FACILITY: '#6b7280' // Gray + FACILITY: '#6b7280', // Gray + DEFAULT: '#9ca3af' // Fallback }; // Status colors for positions @@ -21,14 +22,14 @@ const STATUS_COLORS: Record = { EMPTY: '#4b5563', OCCUPIED: '#22c55e', RESERVED: '#3b82f6', - DAMAGED: '#dc2626' + DAMAGED: '#dc2626', + DEFAULT: '#6b7280' }; -// Scale factor - convert pixels to 3D units const SCALE = 0.01; -// Ground plane component function Ground({ width, height }: { width: number; height: number }) { + if (!width || !height) return null; return ( @@ -37,8 +38,8 @@ function Ground({ width, height }: { width: number; height: number }) { ); } -// Grid overlay function GridOverlay({ width, height }: { width: number; height: number }) { + if (!width || !height) return null; return ( void; - isSelected: boolean; -}) { - const color = ROOM_COLORS[room.type] || '#6b7280'; +function Room3DBox({ room, onClick, isSelected }: { room: Room3D; onClick: () => void; isSelected: boolean }) { + if (!room) return null; + const color = ROOM_COLORS[room.type] || ROOM_COLORS.DEFAULT; const mesh = useRef(null); - // Center the room - const x = room.posX * SCALE + (room.width * SCALE) / 2; - const z = room.posY * SCALE + (room.height * SCALE) / 2; - const roomHeight = 2; // Room height in 3D units + // Safely calculate position + const x = (room.posX || 0) * SCALE + ((room.width || 0) * SCALE) / 2; + const z = (room.posY || 0) * SCALE + ((room.height || 0) * SCALE) / 2; + const roomHeight = 2; return ( {/* Floor */} - - - + + + {/* Walls (outline) */} - + {/* Room label */} - - {room.code} - + {room.code && ( + + {room.code} + + )} {/* Sections */} - {room.sections.map(section => ( - + {room.sections?.map(section => ( + ))} ); } -// Section (table/rack) component -function Section3DBox({ section, roomX, roomY }: { - section: Section3D; - roomX: number; - roomY: number; -}) { - const x = (roomX + section.posX) * SCALE + (section.width * SCALE) / 2; - const z = (roomY + section.posY) * SCALE + (section.height * SCALE) / 2; +function Section3DBox({ section, roomX, roomY }: { section: Section3D; roomX: number; roomY: number }) { + if (!section) return null; + const x = (roomX + (section.posX || 0)) * SCALE + ((section.width || 0) * SCALE) / 2; + const z = (roomY + (section.posY || 0)) * SCALE + ((section.height || 0) * SCALE) / 2; const tableHeight = 0.5; return ( {/* Table surface */} - + {/* Positions (plants) */} - {section.positions.map((pos, idx) => { - const cellWidth = section.width / section.columns; - const cellHeight = section.height / section.rows; - const posX = (roomX + section.posX + (pos.column - 0.5) * cellWidth) * SCALE; - const posZ = (roomY + section.posY + (pos.row - 0.5) * cellHeight) * SCALE; + {section.positions?.map(pos => { + const rows = section.rows || 1; + const cols = section.columns || 1; + const cellWidth = (section.width || 0) / cols; + const cellHeight = (section.height || 0) / rows; + const posX = (roomX + (section.posX || 0) + ((pos.column || 1) - 0.5) * cellWidth) * SCALE; + const posZ = (roomY + (section.posY || 0) + ((pos.row || 1) - 0.5) * cellHeight) * SCALE; - return ( - - ); + return ; })} ); } -// Plant position component -function PlantPosition({ position, x, y, z }: { - position: Position3D; - x: number; - y: number; - z: number; -}) { - const color = STATUS_COLORS[position.status] || STATUS_COLORS.EMPTY; +function PlantPosition({ position, x, y, z }: { position: Position3D; x: number; y: number; z: number }) { + if (!position) return null; + const color = STATUS_COLORS[position.status] || STATUS_COLORS.DEFAULT; const isOccupied = position.status === 'OCCUPIED'; const [hovered, setHovered] = useState(false); return ( - setHovered(true)} - onPointerOut={() => setHovered(false)} - > - {isOccupied ? ( - - ) : ( - - )} - + setHovered(true)} onPointerOut={() => setHovered(false)}> + {isOccupied ? : } + ); } -// Camera controller for auto-framing function CameraController({ floorWidth, floorHeight }: { floorWidth: number; floorHeight: number }) { const { camera } = useThree(); - useEffect(() => { + if (!floorWidth || !floorHeight) return; const maxDim = Math.max(floorWidth, floorHeight) * SCALE; camera.position.set(maxDim * 0.8, maxDim * 1.2, maxDim * 0.8); camera.lookAt(floorWidth * SCALE / 2, 0, floorHeight * SCALE / 2); }, [camera, floorWidth, floorHeight]); - return null; } -// Main 3D Scene -function Facility3DScene({ data, selectedRoomId, onSelectRoom }: { - data: Floor3DData; - selectedRoomId: string | null; - onSelectRoom: (id: string | null) => void; -}) { +function Facility3DScene({ data, selectedRoomId, onSelectRoom }: { data: Floor3DData; selectedRoomId: string | null; onSelectRoom: (id: string | null) => void; }) { + if (!data?.floor) return null; return ( <> {/* Lighting */} - - - + + {/* Ground and grid */} {/* Rooms */} - {data.rooms.map(room => ( + {data.rooms?.map(room => ( - + ); } -// Floor selector dropdown -function FloorSelector({ - floors, - currentFloorId, - onSelect -}: { - floors: { id: string; name: string; buildingName: string }[]; - currentFloorId: string | null; - onSelect: (id: string) => void; -}) { +function FloorSelector({ floors, currentFloorId, onSelect }: { floors: any[]; currentFloorId: string | null; onSelect: (id: string) => void; }) { const [isOpen, setIsOpen] = useState(false); const current = floors.find(f => f.id === currentFloorId); return (
-