diff --git a/.agent/workflows/deploy.md b/.agent/workflows/deploy.md new file mode 100644 index 0000000..ddbc5c1 --- /dev/null +++ b/.agent/workflows/deploy.md @@ -0,0 +1,124 @@ +--- +description: Deploy ca-grow-ops-manager to nexus-vector via Forgejo +--- + +# Deploying CA Grow Ops Manager + +## Prerequisites + +- SSH access to nexus-vector (via Tailscale) +- Forgejo accessible at +- Deploy key authorized for the repository + +## Standard Deployment (CI/CD Automatic) + +Pushing to `main` branch triggers automatic deployment via Forgejo Actions: + +```bash +# From local machine +cd /Users/ten/ANTIGRAVITY/777wolfpack/ca-grow-ops-manager +git add -A +git commit -m "your commit message" +git push origin main +``` + +The CI/CD workflow will: + +1. Pull latest code on nexus-vector +2. Rebuild containers +3. Run database migrations +4. Seed demo data (if needed) +5. Restart services + +## Manual Deployment (When CI/CD Fails) + +// turbo-all + +### 1. SSH into nexus-vector + +```bash +ssh nexus-vector +``` + +### 2. Navigate to project + +```bash +cd /srv/containers/ca-grow-ops-manager +``` + +### 3. Pull latest code (use admin user for Forgejo access) + +```bash +# If deploy key is set up: +sudo -u admin git pull origin main + +# If that fails, use HTTPS with token: +git pull https://git.runfoo.run/malty/ca-grow-ops-manager.git main +``` + +### 4. Rebuild and deploy + +```bash +docker compose build --no-cache +docker compose up -d db redis +sleep 5 +docker compose run --rm backend npx prisma migrate deploy +docker compose run --rm backend node prisma/seed.js +docker compose up -d +``` + +### 5. Verify deployment + +```bash +docker compose ps +curl -s http://localhost:8010/api/healthz +``` + +## Troubleshooting + +### Forgejo 502 Error + +Forgejo container may need restart: + +```bash +cd /srv/containers/forgejo +docker compose restart forgejo +``` + +### Git Permission Denied + +Deploy key not authorized. Either: + +1. Add deploy key to repo in Forgejo UI (Settings > Deploy Keys) +2. Use HTTPS with personal access token + +### SSH Host Key Changed + +```bash +ssh-keygen -f ~/.ssh/known_hosts -R '[localhost]:2222' +ssh-keyscan -p 2222 localhost >> ~/.ssh/known_hosts +``` + +## Useful Commands + +```bash +# View logs +docker compose logs -f frontend +docker compose logs -f backend + +# Check container health +docker compose ps + +# Run database commands +docker compose exec db psql -U ca_grow_ops -d ca_grow_ops + +# Restart specific service +docker compose restart backend +``` + +## URLs + +- **Live Site**: +- **API Health**: +- **Forgejo**: +- **Forgejo SSH**: ssh://git@localhost:2222/malty/ca-grow-ops-manager.git (from nexus-vector) diff --git a/frontend/src/features/layout-designer/LayoutDesignerPage.tsx b/frontend/src/features/layout-designer/LayoutDesignerPage.tsx index c3f555b..88a3d4b 100644 --- a/frontend/src/features/layout-designer/LayoutDesignerPage.tsx +++ b/frontend/src/features/layout-designer/LayoutDesignerPage.tsx @@ -12,6 +12,7 @@ import { ElementsPanel } from './components/ElementsPanel'; import { InspectorPanel } from './components/InspectorPanel'; import { FloorSelector } from './components/FloorSelector'; import { PropertySetup } from './components/PropertySetup'; +import { AddFloorModal } from './components/AddFloorModal'; import { PlantInventory } from './components/PlantInventory'; import { layoutApi } from '../../lib/layoutApi'; @@ -40,6 +41,8 @@ export default function LayoutDesignerPage() { const [saving, setSaving] = useState(false); const [showFloorSelector, setShowFloorSelector] = useState(false); const [showPropertySetup, setShowPropertySetup] = useState(false); + const [showAddFloorModal, setShowAddFloorModal] = useState(false); + const [buildingForFloor, setBuildingForFloor] = useState(null); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [sidebarMode, setSidebarMode] = useState<'design' | 'plants'>('design'); @@ -491,12 +494,29 @@ export default function LayoutDesignerPage() { isOpen={showFloorSelector} onClose={() => setShowFloorSelector(false)} onSelectFloor={handleSelectFloor} + onAddProperty={() => setShowPropertySetup(true)} + onAddFloor={(bId) => { + setBuildingForFloor(bId); + setShowAddFloorModal(true); + }} /> setShowPropertySetup(false)} onCreated={loadProperties} /> + setShowAddFloorModal(false)} + buildingId={buildingForFloor} + onCreated={(newFloorId) => { + loadProperties().then(() => { + if (buildingForFloor) { + handleSelectFloor(newFloorId, buildingForFloor); + } + }); + }} + /> ); } diff --git a/frontend/src/features/layout-designer/components/AddFloorModal.tsx b/frontend/src/features/layout-designer/components/AddFloorModal.tsx new file mode 100644 index 0000000..aae875b --- /dev/null +++ b/frontend/src/features/layout-designer/components/AddFloorModal.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react'; +import { X, Layers, ArrowRight, Loader2 } from 'lucide-react'; +import { layoutApi } from '../../../lib/layoutApi'; + +interface AddFloorModalProps { + isOpen: boolean; + onClose: () => void; + buildingId: string | null; + onCreated: (floorId: string) => void; +} + +export function AddFloorModal({ isOpen, onClose, buildingId, onCreated }: AddFloorModalProps) { + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + name: '', + number: 1, + width: 100, + height: 100 + }); + + if (!isOpen || !buildingId) return null; + + const handleCreate = async () => { + if (!formData.name.trim()) return; + + setLoading(true); + try { + const floor = await layoutApi.createFloor(buildingId, { + name: formData.name.trim(), + number: formData.number, + width: formData.width, + height: formData.height + }); + onCreated(floor.id); + onClose(); + // Reset form + setFormData({ name: '', number: formData.number + 1, width: 100, height: 100 }); + } catch (error) { + console.error('Failed to create floor:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Add New Floor

+

Define floor dimensions

+
+
+ +
+ + {/* Content */} +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Floor 2" + className="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 text-white focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 outline-none" + /> +
+ +
+
+ + setFormData({ ...formData, number: parseInt(e.target.value) || 0 })} + className="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 text-white focus:border-emerald-500 outline-none" + /> +
+
+ + setFormData({ ...formData, width: parseInt(e.target.value) || 0 })} + className="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 text-white focus:border-emerald-500 outline-none" + /> +
+
+ + setFormData({ ...formData, height: parseInt(e.target.value) || 0 })} + className="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 text-white focus:border-emerald-500 outline-none" + /> +
+
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/frontend/src/features/layout-designer/components/FloorSelector.tsx b/frontend/src/features/layout-designer/components/FloorSelector.tsx index b590d44..e967aff 100644 --- a/frontend/src/features/layout-designer/components/FloorSelector.tsx +++ b/frontend/src/features/layout-designer/components/FloorSelector.tsx @@ -6,9 +6,17 @@ interface FloorSelectorProps { isOpen: boolean; onClose: () => void; onSelectFloor: (floorId: string, buildingId: string) => void; + onAddProperty?: () => void; + onAddFloor?: (buildingId: string) => void; } -export function FloorSelector({ isOpen, onClose, onSelectFloor }: FloorSelectorProps) { +export function FloorSelector({ + isOpen, + onClose, + onSelectFloor, + onAddProperty, + onAddFloor +}: FloorSelectorProps) { const { property, buildingId, floorId } = useLayoutStore(); const [expandedBuildings, setExpandedBuildings] = useState>(new Set()); @@ -49,17 +57,28 @@ export function FloorSelector({ isOpen, onClose, onSelectFloor }: FloorSelectorP {/* Property Info */} - {property && ( -
+
+
-

{property.name}

- {property.address && ( +

{property?.name || 'No Property'}

+ {property?.address && (

{property.address}

)}
- )} + {onAddProperty && ( + + )} +
{/* Buildings & Floors List */}
@@ -116,10 +135,18 @@ export function FloorSelector({ isOpen, onClose, onSelectFloor }: FloorSelectorP })} {/* Add Floor Button */} - + {onAddFloor && ( + + )}
)}
@@ -130,7 +157,17 @@ export function FloorSelector({ isOpen, onClose, onSelectFloor }: FloorSelectorP

No buildings found

-

Create a property first

+
)}