diff --git a/apps/editor/app/editor/[projectId]/page.tsx b/apps/editor/app/editor/[projectId]/page.tsx
index 09102acc..2a013627 100644
--- a/apps/editor/app/editor/[projectId]/page.tsx
+++ b/apps/editor/app/editor/[projectId]/page.tsx
@@ -1,24 +1,33 @@
'use client'
import Editor from '@/components/editor'
-import { useParams } from 'next/navigation'
-import { useEffect, useLayoutEffect } from 'react'
+import { useParams, useRouter } from 'next/navigation'
+import { useLayoutEffect } from 'react'
import { useProjectStore } from '@/features/community/lib/projects/store'
import { useAuth } from '@/features/community/lib/auth/hooks'
export default function EditorPage() {
const params = useParams()
const projectId = params.projectId as string
- const { isAuthenticated } = useAuth()
+ const { isAuthenticated, isLoading } = useAuth()
const setActiveProject = useProjectStore((state) => state.setActiveProject)
+ const router = useRouter()
// Use layoutEffect to set active project BEFORE the editor renders and hooks run
useLayoutEffect(() => {
- // For authenticated users with cloud projects, set the active project from URL
- if (isAuthenticated && projectId && !projectId.startsWith('local_')) {
+ if (isLoading) return
+ if (!isAuthenticated) {
+ router.replace('/')
+ return
+ }
+ if (projectId) {
setActiveProject(projectId)
}
- }, [projectId, isAuthenticated, setActiveProject])
+ }, [projectId, isAuthenticated, isLoading, setActiveProject, router])
+
+ if (isLoading || !isAuthenticated) {
+ return null
+ }
return (
diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx
index 40edca0c..6509a582 100644
--- a/apps/editor/components/editor/index.tsx
+++ b/apps/editor/components/editor/index.tsx
@@ -2,8 +2,6 @@
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
import { Viewer } from '@pascal-app/viewer'
-import { useAuth } from '@/features/community/lib/auth/hooks'
-import { useLocalProjectScene } from '@/features/community/lib/local-storage/hooks'
import { useProjectScene } from '@/features/community/lib/models/hooks'
import { useKeyboard } from '@/hooks/use-keyboard'
import { initSFXBus } from '@/lib/sfx-bus'
@@ -38,18 +36,7 @@ interface EditorProps {
export default function Editor({ projectId }: EditorProps) {
useKeyboard()
- const { isAuthenticated } = useAuth()
-
- // Determine which mode to use
- const isLocalProject = projectId?.startsWith('local_')
- const shouldUseCloud = isAuthenticated && !isLocalProject
- const shouldUseLocal = !shouldUseCloud && !!projectId
-
- // Call hooks unconditionally (hooks internally check if they should activate)
- // Cloud hook activates when there's an activeProject in the store
useProjectScene()
- // Local hook activates when projectId is provided and starts with 'local_'
- useLocalProjectScene(shouldUseLocal ? projectId : undefined)
return (
diff --git a/apps/editor/components/tools/wall/wall-tool.tsx b/apps/editor/components/tools/wall/wall-tool.tsx
index abe04962..8dc325fa 100644
--- a/apps/editor/components/tools/wall/wall-tool.tsx
+++ b/apps/editor/components/tools/wall/wall-tool.tsx
@@ -143,6 +143,9 @@ export const WallTool: React.FC = () => {
buildingState.current = 1
wallPreviewRef.current.visible = true
} else if (buildingState.current === 1) {
+ const dx = endingPoint.current.x - startingPoint.current.x
+ const dz = endingPoint.current.z - startingPoint.current.z
+ if (dx * dx + dz * dz < 0.01 * 0.01) return
commitWallDrawing(
[startingPoint.current.x, startingPoint.current.z],
[endingPoint.current.x, endingPoint.current.z],
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx
index 6d384fff..949c084f 100644
--- a/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import {
type AnyNodeId,
type GuideNode,
@@ -5,12 +7,13 @@ import {
type LevelNode,
type ScanNode,
ScanNode as ScanNodeSchema,
- saveAsset,
useScene,
} from '@pascal-app/core'
import { Box, Image, Pencil, Plus, Trash2 } from 'lucide-react'
-import { useCallback, useRef } from 'react'
+import { useCallback, useRef, useState } from 'react'
import useEditor from '@/store/use-editor'
+import { deleteProjectAssetByUrl, uploadProjectAsset } from '@/features/community/lib/assets/actions'
+import { useProjectStore } from '@/features/community/lib/projects/store'
import {
Dialog,
DialogContent,
@@ -23,6 +26,8 @@ import {
PopoverTrigger,
} from '@/components/ui/primitives/popover'
+const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB — matches server action bodySizeLimit
+
interface ReferencesDialogProps {
levelId: string
open: boolean
@@ -34,6 +39,9 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
const createNode = useScene((s) => s.createNode)
const deleteNode = useScene((s) => s.deleteNode)
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
+ const activeProject = useProjectStore((s) => s.activeProject)
+ const [uploadError, setUploadError] = useState(null)
+ const [uploading, setUploading] = useState(false)
const scanInputRef = useRef(null)
const guideInputRef = useRef(null)
@@ -42,38 +50,80 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
async (e: React.ChangeEvent) => {
const file = e.target.files?.[0]
if (!file) return
- const url = await saveAsset(file)
+ e.target.value = ''
+
+ const projectId = activeProject?.id
+ if (!projectId) {
+ setUploadError('No active project. Please open a project first.')
+ return
+ }
+
+ if (file.size > MAX_FILE_SIZE) {
+ setUploadError(`File is too large (${(file.size / 1024 / 1024).toFixed(0)} MB). Maximum size is 100 MB.`)
+ return
+ }
+
+ setUploadError(null)
+ setUploading(true)
+ const result = await uploadProjectAsset(projectId, file, 'scan')
+ setUploading(false)
+
+ if (!result.success) {
+ setUploadError(result.error)
+ return
+ }
+
const node = ScanNodeSchema.parse({
- url,
+ url: result.url,
name: file.name,
parentId: levelId,
})
createNode(node, levelId as AnyNodeId)
- e.target.value = ''
// Auto-select and close dialog
setSelectedReferenceId(node.id)
onOpenChange(false)
},
- [levelId, createNode, setSelectedReferenceId, onOpenChange],
+ [levelId, createNode, setSelectedReferenceId, onOpenChange, activeProject],
)
const handleAddGuide = useCallback(
async (e: React.ChangeEvent) => {
const file = e.target.files?.[0]
if (!file) return
- const url = await saveAsset(file)
+ e.target.value = ''
+
+ const projectId = activeProject?.id
+ if (!projectId) {
+ setUploadError('No active project. Please open a project first.')
+ return
+ }
+
+ if (file.size > MAX_FILE_SIZE) {
+ setUploadError(`File is too large (${(file.size / 1024 / 1024).toFixed(0)} MB). Maximum size is 100 MB.`)
+ return
+ }
+
+ setUploadError(null)
+ setUploading(true)
+ const result = await uploadProjectAsset(projectId, file, 'guide')
+ setUploading(false)
+
+ if (!result.success) {
+ setUploadError(result.error)
+ return
+ }
+
const node = GuideNodeSchema.parse({
- url,
+ url: result.url,
name: file.name,
parentId: levelId,
})
createNode(node, levelId as AnyNodeId)
- e.target.value = ''
// Auto-select and close dialog
setSelectedReferenceId(node.id)
onOpenChange(false)
},
- [levelId, createNode, setSelectedReferenceId, onOpenChange],
+ [levelId, createNode, setSelectedReferenceId, onOpenChange, activeProject],
)
const handleEdit = useCallback(
@@ -85,10 +135,26 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
)
const handleDelete = useCallback(
- (nodeId: string) => {
+ async (nodeId: string) => {
+ const refNode = nodes[nodeId as AnyNodeId] as ScanNode | GuideNode | undefined
+ const projectId = activeProject?.id
+
+ // Delete storage asset first (before removing from scene)
+ if (
+ projectId &&
+ refNode?.url &&
+ (refNode.url.startsWith('http://') || refNode.url.startsWith('https://'))
+ ) {
+ const result = await deleteProjectAssetByUrl(projectId, refNode.url)
+ if (!result.success) {
+ setUploadError(`Failed to delete asset: ${result.error}`)
+ return
+ }
+ }
+
deleteNode(nodeId as AnyNodeId)
},
- [deleteNode],
+ [deleteNode, nodes, activeProject],
)
const level = nodes[levelId as AnyNodeId] as LevelNode | undefined
@@ -145,6 +211,10 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
))}
+ {uploadError && (
+
{uploadError}
+ )}
+
-
diff --git a/apps/editor/features/community/components/cloud-save-button.tsx b/apps/editor/features/community/components/cloud-save-button.tsx
index 7a3d4f2c..750339ee 100644
--- a/apps/editor/features/community/components/cloud-save-button.tsx
+++ b/apps/editor/features/community/components/cloud-save-button.tsx
@@ -1,32 +1,23 @@
'use client'
-import { Cloud, Home } from 'lucide-react'
-import { useEffect, useState } from 'react'
+import { Home } from 'lucide-react'
import { useRouter } from 'next/navigation'
+import { useEffect } from 'react'
import { useAuth } from '../lib/auth/hooks'
import { useProjectStore } from '../lib/projects/store'
import { ProfileDropdown } from './profile-dropdown'
-import { SignInDialog } from './sign-in-dialog'
-
-interface CloudSaveButtonProps {
- projectId?: string
-}
/**
* CloudSaveButton - Shows authentication state and project management
*
- * Guest with local project: Shows "Save to cloud" button
- * Guest without project: Shows "Home" button
+ * Guest: Shows "Home" button
* Authenticated: Shows ProfileDropdown
*/
-export function CloudSaveButton({ projectId }: CloudSaveButtonProps) {
+export function CloudSaveButton() {
const { isAuthenticated, isLoading } = useAuth()
- const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
const initialize = useProjectStore(state => state.initialize)
const router = useRouter()
- const isLocalProject = projectId?.startsWith('local_')
-
// Initialize project store when authenticated
useEffect(() => {
if (isAuthenticated) {
@@ -44,25 +35,6 @@ export function CloudSaveButton({ projectId }: CloudSaveButtonProps) {
)
}
- // Guest user with local project
- if (!isAuthenticated && isLocalProject) {
- return (
- <>
-
- setIsSignInDialogOpen(true)}
- >
-
- Save to cloud
-
-
-
- >
- )
- }
-
- // Guest user (no project context or browsing)
if (!isAuthenticated) {
return (
@@ -77,7 +49,6 @@ export function CloudSaveButton({ projectId }: CloudSaveButtonProps) {
)
}
- // Authenticated user
return (
diff --git a/apps/editor/features/community/components/community-hub.tsx b/apps/editor/features/community/components/community-hub.tsx
index 839fdc9b..4c2e235d 100644
--- a/apps/editor/features/community/components/community-hub.tsx
+++ b/apps/editor/features/community/components/community-hub.tsx
@@ -4,8 +4,6 @@ import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useAuth } from '../lib/auth/hooks'
-import type { LocalProject } from '../lib/local-storage/project-store'
-import { createLocalProject, getLocalProjects } from '../lib/local-storage/project-store'
import { getPublicProjects, getUserProjects } from '../lib/projects/actions'
import type { Project } from '../lib/projects/types'
import { CreateProjectButton } from './create-project-button'
@@ -16,27 +14,23 @@ import { ProjectGrid } from './project-grid'
import { SignInDialog } from './sign-in-dialog'
export default function CommunityHub() {
- const { isAuthenticated, isLoading: authLoading, user } = useAuth()
+ const { isAuthenticated, isLoading: authLoading } = useAuth()
const router = useRouter()
const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
const [isNewProjectDialogOpen, setIsNewProjectDialogOpen] = useState(false)
- const [localProjectToSave, setLocalProjectToSave] = useState
(null)
const [publicProjects, setPublicProjects] = useState([])
const [userProjects, setUserProjects] = useState([])
- const [localProjects, setLocalProjects] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function loadProjects() {
setLoading(true)
- // Load public projects (always)
const publicResult = await getPublicProjects()
if (publicResult.success) {
setPublicProjects(publicResult.data || [])
}
- // Load user projects if authenticated
if (isAuthenticated) {
const userResult = await getUserProjects()
if (userResult.success) {
@@ -44,9 +38,6 @@ export default function CommunityHub() {
}
}
- // Always load local projects
- setLocalProjects(getLocalProjects())
-
setLoading(false)
}
@@ -55,27 +46,7 @@ export default function CommunityHub() {
}
}, [isAuthenticated, authLoading])
- const handleCreateProject = async () => {
- if (!isAuthenticated) {
- // Create local project for guest
- const project = createLocalProject('Untitled Project')
- router.push(`/editor/${project.id}`)
- } else {
- // Open project creation dialog for authenticated users
- setIsNewProjectDialogOpen(true)
- }
- }
-
const handleProjectCreated = async (projectId: string) => {
- // If this was a local project being saved, delete it from localStorage
- if (localProjectToSave) {
- const { deleteLocalProject } = await import('../lib/local-storage/project-store')
- deleteLocalProject(localProjectToSave.id)
- setLocalProjects(getLocalProjects())
- setLocalProjectToSave(null)
- }
-
- // Reload projects and navigate to the new project
const result = await getUserProjects()
if (result.success) {
setUserProjects(result.data || [])
@@ -83,11 +54,6 @@ export default function CommunityHub() {
router.push(`/editor/${projectId}`)
}
- const handleSaveLocalToCloud = (localProject: LocalProject) => {
- setLocalProjectToSave(localProject)
- setIsNewProjectDialogOpen(true)
- }
-
const handleProjectClick = (projectId: string) => {
router.push(`/editor/${projectId}`)
}
@@ -153,18 +119,17 @@ export default function CommunityHub() {
My Projects
-
+ setIsNewProjectDialogOpen(true)} />
- {userProjects.length === 0 && localProjects.length === 0 ? (
+ {userProjects.length === 0 ? (
You don't have any projects yet.
) : (
{
@@ -181,30 +146,19 @@ export default function CommunityHub() {
)}
- {/* Local Projects Section (Guest Users) */}
- {!isAuthenticated && localProjects.length > 0 && (
-
-
-
My Local Projects
-
-
-
-
- )}
-
- {/* Create First Project CTA */}
- {!isAuthenticated && localProjects.length === 0 && (
-
- Get Started
-
- Create your first project to start designing
+ {/* Sign-in CTA for unauthenticated users */}
+ {!isAuthenticated && (
+
+ Build with Pascal
+
+ Create and share 3D architectural projects. Sign in to get started.
-
+ setIsSignInDialogOpen(true)}
+ className="rounded-lg bg-primary px-6 py-2.5 text-primary-foreground font-medium hover:bg-primary/90 transition-colors"
+ >
+ Sign in to create a project
+
)}
@@ -230,15 +184,6 @@ export default function CommunityHub() {
open={isNewProjectDialogOpen}
onOpenChange={setIsNewProjectDialogOpen}
onSuccess={handleProjectCreated}
- localProjectData={
- localProjectToSave
- ? {
- id: localProjectToSave.id,
- name: localProjectToSave.name,
- sceneGraph: localProjectToSave.scene_graph,
- }
- : undefined
- }
/>
)
diff --git a/apps/editor/features/community/components/local-project-migration-dialog.tsx b/apps/editor/features/community/components/local-project-migration-dialog.tsx
deleted file mode 100644
index 277b5670..00000000
--- a/apps/editor/features/community/components/local-project-migration-dialog.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/primitives/dialog'
-import type { LocalProject } from '../lib/local-storage/project-store'
-
-interface LocalProjectMigrationDialogProps {
- localProjects: LocalProject[]
- open: boolean
- onMigrate: () => Promise
- onSkip: () => void
-}
-
-export function LocalProjectMigrationDialog({
- localProjects,
- open,
- onMigrate,
- onSkip,
-}: LocalProjectMigrationDialogProps) {
- const [isMigrating, setIsMigrating] = useState(false)
-
- const handleMigrate = async () => {
- setIsMigrating(true)
- try {
- await onMigrate()
- } finally {
- setIsMigrating(false)
- }
- }
-
- return (
-
- )
-}
diff --git a/apps/editor/features/community/components/new-project-dialog.tsx b/apps/editor/features/community/components/new-project-dialog.tsx
index 88e40637..5b5fcc63 100644
--- a/apps/editor/features/community/components/new-project-dialog.tsx
+++ b/apps/editor/features/community/components/new-project-dialog.tsx
@@ -2,26 +2,21 @@
import { X } from 'lucide-react'
import { useState } from 'react'
-import { createProject } from '../lib/projects/actions'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog'
import { Switch } from '@/components/ui/primitives/switch'
+import { createProject } from '../lib/projects/actions'
interface NewProjectDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onSuccess?: (projectId: string) => void
- localProjectData?: {
- id: string
- name: string
- sceneGraph: any
- }
}
/**
- * NewProjectDialog - Dialog for creating a new project with optional Google Maps address search
+ * NewProjectDialog - Dialog for creating a new project
*/
-export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectData }: NewProjectDialogProps) {
- const [projectName, setProjectName] = useState(localProjectData?.name || '')
+export function NewProjectDialog({ open, onOpenChange, onSuccess }: NewProjectDialogProps) {
+ const [projectName, setProjectName] = useState('')
const [isPrivate, setIsPrivate] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const [error, setError] = useState(null)
@@ -32,19 +27,10 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
const name = projectName.trim() || 'Untitled Project'
- if (!name) {
- setError('Please enter a project name')
- return
- }
-
setIsCreating(true)
try {
- const result = await createProject({
- name,
- isPrivate,
- sceneGraph: localProjectData?.sceneGraph,
- })
+ const result = await createProject({ name, isPrivate })
if (result.success && result.data) {
onOpenChange(false)
@@ -73,7 +59,7 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
return (
- {localProjectData && (
-
-
- Saving local project: {localProjectData.name}
-
-
- Your building data will be preserved
-
-
- )}
-
{error && (
{error}
diff --git a/apps/editor/features/community/components/project-dropdown.tsx b/apps/editor/features/community/components/project-dropdown.tsx
index fee0a974..2fc35f27 100644
--- a/apps/editor/features/community/components/project-dropdown.tsx
+++ b/apps/editor/features/community/components/project-dropdown.tsx
@@ -1,6 +1,7 @@
'use client'
import { Check, ChevronDown, Home, Plus } from 'lucide-react'
+import { useRouter } from 'next/navigation'
import { useState } from 'react'
import {
DropdownMenu,
@@ -18,6 +19,8 @@ import { NewProjectDialog } from './new-project-dialog'
* Having it in both places caused duplicate subscriptions and 2x server action calls.
*/
export function ProjectDropdown() {
+ const router = useRouter()
+
// Use project store
const projects = useProjectStore((state) => state.projects)
const activeProject = useProjectStore((state) => state.activeProject)
@@ -35,9 +38,8 @@ export function ProjectDropdown() {
setIsNewProjectDialogOpen(true)
}
- const handleProjectCreated = async (projectId: string) => {
- // Set the newly created project as active (this will also fetch projects)
- await setActiveProject(projectId)
+ const handleProjectCreated = (projectId: string) => {
+ router.push(`/editor/${projectId}`)
}
return (
diff --git a/apps/editor/features/community/components/project-grid.tsx b/apps/editor/features/community/components/project-grid.tsx
index d4e785bc..9b594f73 100644
--- a/apps/editor/features/community/components/project-grid.tsx
+++ b/apps/editor/features/community/components/project-grid.tsx
@@ -3,34 +3,25 @@
import { Eye, Heart, Settings } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
-import type { Project } from '../lib/projects/types'
-import type { LocalProject } from '../lib/local-storage/project-store'
-import { ProjectSettingsDialog } from './project-settings-dialog'
-import { getUserProjectLikes, toggleProjectLike } from '../lib/projects/actions'
import { useAuth } from '../lib/auth/hooks'
+import { getUserProjectLikes, toggleProjectLike } from '../lib/projects/actions'
+import type { Project } from '../lib/projects/types'
+import { ProjectSettingsDialog } from './project-settings-dialog'
interface ProjectGridProps {
- projects: (Project | LocalProject)[]
+ projects: Project[]
onProjectClick: (id: string) => void
onViewClick?: (id: string) => void
- onSaveToCloud?: (project: LocalProject) => void
showOwner: boolean
- isLocal?: boolean
canEdit?: boolean
onUpdate?: () => void
}
-function isLocalProject(prop: Project | LocalProject): prop is LocalProject {
- return 'is_local' in prop && prop.is_local === true
-}
-
export function ProjectGrid({
projects,
onProjectClick,
onViewClick,
- onSaveToCloud,
showOwner,
- isLocal = false,
canEdit = false,
onUpdate,
}: ProjectGridProps) {
@@ -39,28 +30,21 @@ export function ProjectGrid({
const [userLikes, setUserLikes] = useState
>({})
const [likeCounts, setLikeCounts] = useState>({})
- // Initialize like counts from projects
useEffect(() => {
const counts: Record = {}
projects.forEach((proj) => {
- if (!isLocalProject(proj)) {
- counts[proj.id] = proj.likes
- }
+ counts[proj.id] = proj.likes
})
setLikeCounts(counts)
}, [projects])
- // Fetch which projects the user has liked
useEffect(() => {
if (!isAuthenticated) {
setUserLikes({})
return
}
- const projectIds = projects
- .filter((p) => !isLocalProject(p))
- .map((p) => p.id)
-
+ const projectIds = projects.map((p) => p.id)
if (projectIds.length === 0) return
getUserProjectLikes(projectIds).then((result) => {
@@ -70,11 +54,9 @@ export function ProjectGrid({
})
}, [projects, isAuthenticated])
- const handleSettingsClick = (e: React.MouseEvent, project: Project | LocalProject) => {
+ const handleSettingsClick = (e: React.MouseEvent, project: Project) => {
e.stopPropagation()
- if (!isLocalProject(project)) {
- setSettingsProject(project)
- }
+ setSettingsProject(project)
}
const handleViewClick = (e: React.MouseEvent, projectId: string) => {
@@ -85,31 +67,24 @@ export function ProjectGrid({
const handleLikeClick = async (e: React.MouseEvent, projectId: string) => {
e.stopPropagation()
- if (!isAuthenticated) {
- // Could show a sign-in prompt here
- return
- }
+ if (!isAuthenticated) return
- // Optimistic update
const wasLiked = userLikes[projectId] || false
const currentCount = likeCounts[projectId] || 0
setUserLikes((prev) => ({ ...prev, [projectId]: !wasLiked }))
setLikeCounts((prev) => ({
...prev,
- [projectId]: wasLiked ? currentCount - 1 : currentCount + 1
+ [projectId]: wasLiked ? currentCount - 1 : currentCount + 1,
}))
- // Call server action
const result = await toggleProjectLike(projectId)
if (result.success && result.data) {
- // Update with actual values from server
const data = result.data
setUserLikes((prev) => ({ ...prev, [projectId]: data.liked }))
setLikeCounts((prev) => ({ ...prev, [projectId]: data.likes }))
} else {
- // Revert on error
setUserLikes((prev) => ({ ...prev, [projectId]: wasLiked }))
setLikeCounts((prev) => ({ ...prev, [projectId]: currentCount }))
}
@@ -119,7 +94,7 @@ export function ProjectGrid({
<>
{projects.map((project) => {
- const owner = !isLocalProject(project) ? project.owner : null
+ const owner = project.owner
return (
{/* Thumbnail card */}
- {!isLocalProject(project) && project.thumbnail_url ? (
+ {project.thumbnail_url ? (

)}
- {isLocalProject(project) && (
-
- {isAuthenticated && onSaveToCloud ? (
-
{
- e.stopPropagation()
- onSaveToCloud(project)
- }}
- className="bg-blue-500 hover:bg-blue-600 text-white text-xs px-2.5 py-1 rounded-md transition-colors"
- title="Save to cloud"
- >
- Save to cloud
-
- ) : (
-
- Local
-
- )}
-
- )}
- {canEdit && !isLocalProject(project) && (
+ {canEdit && (
{onViewClick && (
- {/* Avatar */}
{showOwner && owner ? (
) : null}
- {/* Name + stats */}
{project.name}
@@ -220,35 +173,26 @@ export function ProjectGrid({
>
{owner.username || owner.name}
- {!isLocalProject(project) &&
·}
- >
- )}
- {!isLocalProject(project) && (
- <>
-
-
- {project.views}
-
·
-
handleLikeClick(e, project.id)}
- className="flex items-center gap-0.5 shrink-0 hover:text-red-500 transition-colors"
- disabled={!isAuthenticated}
- >
-
- {likeCounts[project.id] ?? project.likes}
-
>
)}
- {isLocalProject(project) && (
-
{new Date(project.updated_at).toLocaleDateString()}
- )}
+
+
+ {project.views}
+
+
·
+
handleLikeClick(e, project.id)}
+ className="flex items-center gap-0.5 shrink-0 hover:text-red-500 transition-colors"
+ disabled={!isAuthenticated}
+ >
+
+ {likeCounts[project.id] ?? project.likes}
+
@@ -257,7 +201,6 @@ export function ProjectGrid({
})}
- {/* Settings Dialog */}
{settingsProject && (
{
+ try {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return { success: false, error: 'Not authenticated' }
+ }
+
+ const supabase = await createServerSupabaseClient()
+
+ // Verify the user owns this project
+ const { data: project, error: projectError } = await supabase
+ .from('projects')
+ .select('owner_id')
+ .eq('id', projectId)
+ .single()
+
+ if (projectError || !project) {
+ return { success: false, error: 'Project not found' }
+ }
+
+ if ((project as any).owner_id !== session.user.id) {
+ return { success: false, error: 'Not authorized to upload to this project' }
+ }
+
+ // Derive extension from file name
+ const ext = file.name.includes('.') ? file.name.split('.').pop()! : ''
+ const assetId = createId('asset')
+ const storageKey = ext ? `${projectId}/${assetId}.${ext}` : `${projectId}/${assetId}`
+
+ const arrayBuffer = await file.arrayBuffer()
+ const bytes = new Uint8Array(arrayBuffer)
+
+ const { data: uploadData, error: uploadError } = await supabase.storage
+ .from(BUCKET)
+ .upload(storageKey, bytes, {
+ contentType: file.type || 'application/octet-stream',
+ upsert: false,
+ })
+
+ if (uploadError) {
+ return { success: false, error: `Upload failed: ${uploadError.message}` }
+ }
+
+ const { data: urlData } = supabase.storage
+ .from(BUCKET)
+ .getPublicUrl(uploadData.path)
+
+ const url = urlData.publicUrl
+
+ // Record in project_assets table
+ const { error: insertError } = await (supabase.from('project_assets') as any).insert({
+ id: assetId,
+ project_id: projectId,
+ storage_key: storageKey,
+ url,
+ type,
+ original_name: file.name,
+ mime_type: file.type || null,
+ })
+
+ if (insertError) {
+ // Best-effort cleanup: remove the uploaded file
+ await supabase.storage.from(BUCKET).remove([storageKey])
+ return { success: false, error: `Failed to record asset: ${insertError.message}` }
+ }
+
+ return { success: true, url }
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to upload asset',
+ }
+ }
+}
+
+/**
+ * Delete a project asset by its public URL.
+ * Removes both the storage file and the project_assets row.
+ */
+export async function deleteProjectAssetByUrl(
+ projectId: string,
+ url: string,
+): Promise {
+ try {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return { success: false, error: 'Not authenticated' }
+ }
+
+ const supabase = await createServerSupabaseClient()
+
+ // Verify ownership
+ const { data: project, error: projectError } = await supabase
+ .from('projects')
+ .select('owner_id')
+ .eq('id', projectId)
+ .single()
+
+ if (projectError || !project) {
+ return { success: false, error: 'Project not found' }
+ }
+
+ if ((project as any).owner_id !== session.user.id) {
+ return { success: false, error: 'Not authorized' }
+ }
+
+ // Derive storage_key from the public URL
+ // URL format: https://.supabase.co/storage/v1/object/public/project-assets/
+ const storageKeyFromUrl = url.split(`/${BUCKET}/`)[1]?.split('?')[0]
+
+ if (!storageKeyFromUrl) {
+ return { success: false, error: 'Could not derive storage key from URL' }
+ }
+
+ // Delete from storage directly — remove() is a no-op if the file doesn't exist
+ const { error: storageError } = await supabase.storage.from(BUCKET).remove([storageKeyFromUrl])
+ if (storageError) {
+ return { success: false, error: `Storage delete failed: ${storageError.message}` }
+ }
+
+ // Delete DB row by storage_key scoped to this project
+ const { error: dbError } = await (supabase.from('project_assets') as any)
+ .delete()
+ .eq('project_id', projectId)
+ .eq('storage_key', storageKeyFromUrl)
+
+ if (dbError) {
+ return { success: false, error: `DB delete failed: ${dbError.message}` }
+ }
+
+ return { success: true }
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to delete asset',
+ }
+ }
+}
diff --git a/apps/editor/features/community/lib/local-storage/hooks.ts b/apps/editor/features/community/lib/local-storage/hooks.ts
deleted file mode 100644
index 86fe56d0..00000000
--- a/apps/editor/features/community/lib/local-storage/hooks.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-'use client'
-
-import { type AnyNodeId, initSpatialGridSync, useScene } from '@pascal-app/core'
-import { useViewer } from '@pascal-app/viewer'
-import { useEffect, useRef } from 'react'
-import useEditor from '@/store/use-editor'
-import { getLocalProject, updateLocalProjectScene } from './project-store'
-
-/**
- * Hook for local project scene management (guest users)
- * Loads scene from localStorage and auto-saves changes
- */
-export function useLocalProjectScene(projectId?: string) {
- const saveTimeoutRef = useRef(undefined)
- const currentProjectIdRef = useRef(null)
- const lastProjectIdRef = useRef(null)
-
- // Load scene when project ID changes
- useEffect(() => {
- if (!projectId || !projectId.startsWith('local_')) {
- return
- }
-
- if (lastProjectIdRef.current === projectId) {
- return
- }
-
- lastProjectIdRef.current = projectId
- currentProjectIdRef.current = projectId
-
- const project = getLocalProject(projectId)
-
- if (project?.scene_graph) {
- const { nodes, rootNodeIds } = project.scene_graph
- useScene.getState().setScene(nodes, rootNodeIds as AnyNodeId[])
- initSpatialGridSync()
- } else {
- useScene.getState().clearScene()
- }
-
- useEditor.getState().setPhase('site')
- useViewer.getState().setSelection({
- buildingId: null,
- levelId: null,
- selectedIds: [],
- zoneId: null,
- })
- }, [projectId])
-
- // Auto-save to localStorage with debouncing
- useEffect(() => {
- if (!projectId || !projectId.startsWith('local_')) {
- currentProjectIdRef.current = null
- return
- }
-
- currentProjectIdRef.current = projectId
- let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
-
- const unsubscribe = useScene.subscribe((state) => {
- const currentNodesSnapshot = JSON.stringify(state.nodes)
-
- if (currentNodesSnapshot === lastNodesSnapshot) {
- return
- }
-
- lastNodesSnapshot = currentNodesSnapshot
- const nodes = state.nodes
-
- if (saveTimeoutRef.current) {
- clearTimeout(saveTimeoutRef.current)
- }
-
- // Debounce save by 1 second (faster than cloud save)
- saveTimeoutRef.current = setTimeout(() => {
- const currentId = currentProjectIdRef.current
- if (!currentId) return
-
- const rootNodeIds = useScene.getState().rootNodeIds
- const sceneGraph = { nodes, rootNodeIds }
-
- updateLocalProjectScene(currentId, sceneGraph)
- }, 1000)
- })
-
- return () => {
- if (saveTimeoutRef.current) {
- clearTimeout(saveTimeoutRef.current)
- }
- unsubscribe()
- }
- }, [projectId])
-}
diff --git a/apps/editor/features/community/lib/local-storage/project-store.ts b/apps/editor/features/community/lib/local-storage/project-store.ts
deleted file mode 100644
index 4560fb7d..00000000
--- a/apps/editor/features/community/lib/local-storage/project-store.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * Local storage management for guest users
- * Stores projects and scenes in browser localStorage
- */
-
-import { createId } from '../utils/id-generator'
-
-export interface SceneGraph {
- nodes: Record
- rootNodeIds: string[]
-}
-
-export interface LocalProject {
- id: string // Format: 'local_project_xyz'
- name: string
- created_at: string
- updated_at: string
- scene_graph: SceneGraph | null
- is_local: true
-}
-
-const LOCAL_PROJECTS_KEY = 'pascal_local_projects'
-// Keep old key for migration
-const LEGACY_LOCAL_PROPERTIES_KEY = 'pascal_local_properties'
-
-function migrateLegacyStorage(): void {
- if (typeof window === 'undefined') return
-
- try {
- const legacy = localStorage.getItem(LEGACY_LOCAL_PROPERTIES_KEY)
- if (legacy && !localStorage.getItem(LOCAL_PROJECTS_KEY)) {
- // Migrate old data to new key
- const parsed = JSON.parse(legacy)
- // Update IDs from local_property_* to local_project_*
- const migrated = parsed.map((p: any) => ({
- ...p,
- id: p.id.replace('local_property_', 'local_project_'),
- }))
- localStorage.setItem(LOCAL_PROJECTS_KEY, JSON.stringify(migrated))
- localStorage.removeItem(LEGACY_LOCAL_PROPERTIES_KEY)
- }
- } catch (error) {
- console.error('Failed to migrate legacy local properties:', error)
- }
-}
-
-export function getLocalProjects(): LocalProject[] {
- if (typeof window === 'undefined') return []
-
- migrateLegacyStorage()
-
- try {
- const stored = localStorage.getItem(LOCAL_PROJECTS_KEY)
- return stored ? JSON.parse(stored) : []
- } catch (error) {
- console.error('Failed to load local projects:', error)
- return []
- }
-}
-
-export function getLocalProject(id: string): LocalProject | null {
- const projects = getLocalProjects()
- return projects.find((p) => p.id === id) || null
-}
-
-export function saveLocalProject(project: LocalProject): void {
- const projects = getLocalProjects()
- const index = projects.findIndex((p) => p.id === project.id)
-
- if (index >= 0) {
- projects[index] = { ...project, updated_at: new Date().toISOString() }
- } else {
- projects.push(project)
- }
-
- localStorage.setItem(LOCAL_PROJECTS_KEY, JSON.stringify(projects))
-}
-
-export function createLocalProject(name: string): LocalProject {
- const project: LocalProject = {
- id: createId('local_project'),
- name,
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- scene_graph: null,
- is_local: true,
- }
-
- saveLocalProject(project)
- return project
-}
-
-export function deleteLocalProject(id: string): void {
- const projects = getLocalProjects().filter((p) => p.id !== id)
- localStorage.setItem(LOCAL_PROJECTS_KEY, JSON.stringify(projects))
-}
-
-export function updateLocalProjectScene(id: string, sceneGraph: SceneGraph): void {
- const project = getLocalProject(id)
- if (project) {
- project.scene_graph = sceneGraph
- saveLocalProject(project)
- }
-}
-
-export function migrateLocalProjectsToCloud(userId: string): LocalProject[] {
- // Return local projects that need to be migrated
- // Actual migration handled by separate function
- return getLocalProjects()
-}
-
-export function clearLocalProjects(): void {
- localStorage.removeItem(LOCAL_PROJECTS_KEY)
-}
diff --git a/apps/editor/features/community/lib/models/hooks.ts b/apps/editor/features/community/lib/models/hooks.ts
index 35f02722..7b7025a3 100644
--- a/apps/editor/features/community/lib/models/hooks.ts
+++ b/apps/editor/features/community/lib/models/hooks.ts
@@ -69,24 +69,56 @@ export function useProjectScene() {
// Load the scene graph into the store
const { nodes, rootNodeIds } = result.data.scene_graph
useScene.getState().setScene(nodes, rootNodeIds)
+
+ // Auto-select the first building + level after store is updated
+ const sceneNodes = useScene.getState().nodes as Record
+ const sceneRootIds = useScene.getState().rootNodeIds
+ const siteNode = sceneRootIds[0] ? sceneNodes[sceneRootIds[0]] : null
+ const resolve = (child: any) =>
+ typeof child === 'string' ? sceneNodes[child] : child
+ const firstBuilding = siteNode?.children?.map(resolve).find((n: any) => n?.type === 'building')
+ const firstLevel = firstBuilding?.children?.map(resolve).find((n: any) => n?.type === 'level')
+
+ if (firstBuilding && firstLevel) {
+ useViewer.getState().setSelection({
+ buildingId: firstBuilding.id,
+ levelId: firstLevel.id,
+ selectedIds: [],
+ zoneId: null,
+ })
+ useEditor.getState().setPhase('structure')
+ } else {
+ useEditor.getState().setPhase('site')
+ useViewer.getState().setSelection({
+ buildingId: null,
+ levelId: null,
+ selectedIds: [],
+ zoneId: null,
+ })
+ }
} else {
// No scene found - clear the scene
useScene.getState().clearScene()
+ useEditor.getState().setPhase('site')
+ useViewer.getState().setSelection({
+ buildingId: null,
+ levelId: null,
+ selectedIds: [],
+ zoneId: null,
+ })
}
} catch (error) {
// Fall back to clear scene
useScene.getState().clearScene()
+ useEditor.getState().setPhase('site')
+ useViewer.getState().setSelection({
+ buildingId: null,
+ levelId: null,
+ selectedIds: [],
+ zoneId: null,
+ })
}
- // Reset editor state after loading/clearing scene
- useEditor.getState().setPhase('site')
- useViewer.getState().setSelection({
- buildingId: null,
- levelId: null,
- selectedIds: [],
- zoneId: null,
- })
-
// Allow auto-save again after a tick (let the store update propagate)
requestAnimationFrame(() => {
isLoadingSceneRef.current = false
diff --git a/apps/editor/features/community/lib/projects/actions.ts b/apps/editor/features/community/lib/projects/actions.ts
index 1c3a1ded..a00efd9d 100644
--- a/apps/editor/features/community/lib/projects/actions.ts
+++ b/apps/editor/features/community/lib/projects/actions.ts
@@ -803,76 +803,6 @@ export async function updateProjectAddress(
}
}
-/**
- * Migrate a local project to the cloud
- * Creates a new project with the local project's data
- */
-export async function migrateLocalProject(
- localProject: {
- name: string
- scene_graph: any
- },
-): Promise> {
- try {
- const session = await getSession()
-
- if (!session?.user) {
- return {
- success: false,
- error: 'Not authenticated',
- }
- }
-
- const supabase = await createServerSupabaseClient()
-
- // Create the project without an address (user can add one later via settings)
- const projectId = createId('project')
- const { error: projectError } = await (supabase.from('projects') as any).insert({
- id: projectId,
- name: localProject.name,
- owner_id: session.user.id,
- address_id: null,
- is_private: true, // Default to private
- })
-
- if (projectError) {
- return {
- success: false,
- error: projectError.message,
- }
- }
-
- // Create the model with the scene graph
- if (localProject.scene_graph) {
- const modelId = createId('model')
- const { error: modelError } = await (supabase.from('projects_models') as any).insert({
- id: modelId,
- project_id: projectId,
- version: 1,
- scene_graph: localProject.scene_graph,
- })
-
- if (modelError) {
- return {
- success: false,
- error: modelError.message,
- }
- }
- }
-
- return {
- success: true,
- data: { id: projectId },
- message: 'Project migrated successfully',
- }
- } catch (error) {
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Failed to migrate project',
- }
- }
-}
-
/**
* Delete a project
* Only the owner can delete their project
@@ -911,7 +841,17 @@ export async function deleteProject(projectId: string): Promise {
}
}
- // Delete the project (cascade will delete related records)
+ // Delete project asset files from storage before deleting the project
+ const { data: assets } = await (supabase.from('project_assets') as any)
+ .select('storage_key')
+ .eq('project_id', projectId)
+
+ if (assets && assets.length > 0) {
+ const storageKeys = (assets as { storage_key: string }[]).map((a) => a.storage_key)
+ await supabase.storage.from('project-assets').remove(storageKeys)
+ }
+
+ // Delete the project (cascade will delete related records including project_assets rows)
const { error } = await supabase.from('projects').delete().eq('id', projectId)
if (error) {
diff --git a/apps/editor/instrumentation.ts b/apps/editor/instrumentation.ts
index a5d0ddca..81648c17 100644
--- a/apps/editor/instrumentation.ts
+++ b/apps/editor/instrumentation.ts
@@ -29,5 +29,13 @@ export async function register() {
})
console.log('Created "project-thumbnails" storage bucket')
}
+
+ if (!bucketNames.has('project-assets')) {
+ await supabase.storage.createBucket('project-assets', {
+ public: true,
+ fileSizeLimit: 500 * 1024 * 1024, // 500MB for GLB/GLTF scans
+ })
+ console.log('Created "project-assets" storage bucket')
+ }
}
}
diff --git a/apps/editor/next.config.ts b/apps/editor/next.config.ts
index bd08139b..7e9dfaaa 100644
--- a/apps/editor/next.config.ts
+++ b/apps/editor/next.config.ts
@@ -4,7 +4,7 @@ const nextConfig: NextConfig = {
transpilePackages: ['three', '@pascal-app/viewer', '@pascal-app/core'],
experimental: {
serverActions: {
- bodySizeLimit: '10mb',
+ bodySizeLimit: '100mb',
},
},
images: {
diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts
index 745b8a7a..65773f88 100644
--- a/packages/db/src/schema/index.ts
+++ b/packages/db/src/schema/index.ts
@@ -10,6 +10,7 @@ export * from './feedback/feedback'
// Project tables
export * from './projects/addresses'
+export * from './projects/assets'
export * from './projects/likes'
export * from './projects/models'
export * from './projects/projects'
diff --git a/packages/db/src/schema/projects/assets.ts b/packages/db/src/schema/projects/assets.ts
new file mode 100644
index 00000000..ef2d7444
--- /dev/null
+++ b/packages/db/src/schema/projects/assets.ts
@@ -0,0 +1,25 @@
+import { relations } from 'drizzle-orm'
+import { pgTable } from 'drizzle-orm/pg-core'
+import { id, timestampsColumns } from '../../helpers'
+import { projects } from './projects'
+
+export const projectAssets = pgTable('project_assets', (t) => ({
+ id: id('asset'),
+ projectId: t.text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
+ storageKey: t.text('storage_key').notNull(),
+ url: t.text('url').notNull(),
+ type: t.text('type').notNull(), // 'scan' | 'guide'
+ originalName: t.text('original_name'),
+ mimeType: t.text('mime_type'),
+ ...timestampsColumns,
+})).enableRLS()
+
+export const projectAssetsRelations = relations(projectAssets, ({ one }) => ({
+ project: one(projects, {
+ fields: [projectAssets.projectId],
+ references: [projects.id],
+ }),
+}))
+
+export type ProjectAsset = typeof projectAssets.$inferSelect
+export type NewProjectAsset = typeof projectAssets.$inferInsert
diff --git a/supabase/migrations/20260226002554_big_pestilence.sql b/supabase/migrations/20260226002554_big_pestilence.sql
new file mode 100644
index 00000000..3fdc542f
--- /dev/null
+++ b/supabase/migrations/20260226002554_big_pestilence.sql
@@ -0,0 +1,14 @@
+CREATE TABLE "project_assets" (
+ "id" text PRIMARY KEY NOT NULL,
+ "project_id" text NOT NULL,
+ "storage_key" text NOT NULL,
+ "url" text NOT NULL,
+ "type" text NOT NULL,
+ "original_name" text,
+ "mime_type" text,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "project_assets" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
+ALTER TABLE "project_assets" ADD CONSTRAINT "project_assets_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
\ No newline at end of file
diff --git a/supabase/migrations/meta/20260226002554_snapshot.json b/supabase/migrations/meta/20260226002554_snapshot.json
new file mode 100644
index 00000000..16b6250f
--- /dev/null
+++ b/supabase/migrations/meta/20260226002554_snapshot.json
@@ -0,0 +1,1128 @@
+{
+ "id": "0d2d0fe5-900e-410c-aa0b-e44aa124e5de",
+ "prevId": "709551b6-3bd6-4cc4-bf0f-683ccc08b2b9",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.auth_accounts": {
+ "name": "auth_accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "auth_accounts_user_id_auth_users_id_fk": {
+ "name": "auth_accounts_user_id_auth_users_id_fk",
+ "tableFrom": "auth_accounts",
+ "tableTo": "auth_users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.auth_jwks": {
+ "name": "auth_jwks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key": {
+ "name": "private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.auth_sessions": {
+ "name": "auth_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active_project_id": {
+ "name": "active_project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "auth_sessions_user_id_auth_users_id_fk": {
+ "name": "auth_sessions_user_id_auth_users_id_fk",
+ "tableFrom": "auth_sessions",
+ "tableTo": "auth_users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "auth_sessions_impersonated_by_auth_users_id_fk": {
+ "name": "auth_sessions_impersonated_by_auth_users_id_fk",
+ "tableFrom": "auth_sessions",
+ "tableTo": "auth_users",
+ "columnsFrom": [
+ "impersonated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.auth_users": {
+ "name": "auth_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_url": {
+ "name": "github_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "x_url": {
+ "name": "x_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "youtube_url": {
+ "name": "youtube_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_notifications": {
+ "name": "email_notifications",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "role": {
+ "name": "role",
+ "type": "auth_user_roles",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_unique_index": {
+ "name": "email_unique_index",
+ "columns": [
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "username_unique_index": {
+ "name": "username_unique_index",
+ "columns": [
+ {
+ "expression": "lower(\"username\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.auth_verifications": {
+ "name": "auth_verifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "verification_identifier_index": {
+ "name": "verification_identifier_index",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.feedback": {
+ "name": "feedback",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "images": {
+ "name": "images",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scene_graph": {
+ "name": "scene_graph",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.projects_addresses": {
+ "name": "projects_addresses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "street_number": {
+ "name": "street_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "route": {
+ "name": "route",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "route_short": {
+ "name": "route_short",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "neighborhood": {
+ "name": "neighborhood",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "city": {
+ "name": "city",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "county": {
+ "name": "county",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state_long": {
+ "name": "state_long",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postal_code": {
+ "name": "postal_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postal_code_suffix": {
+ "name": "postal_code_suffix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country_long": {
+ "name": "country_long",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latitude": {
+ "name": "latitude",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "longitude": {
+ "name": "longitude",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raw_json": {
+ "name": "raw_json",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "address_components_unique": {
+ "name": "address_components_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "street_number",
+ "route",
+ "city",
+ "state",
+ "postal_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.project_assets": {
+ "name": "project_assets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_name": {
+ "name": "original_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "project_assets_project_id_projects_id_fk": {
+ "name": "project_assets_project_id_projects_id_fk",
+ "tableFrom": "project_assets",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.projects_likes": {
+ "name": "projects_likes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "projects_likes_project_id_projects_id_fk": {
+ "name": "projects_likes_project_id_projects_id_fk",
+ "tableFrom": "projects_likes",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "projects_likes_project_user_unique": {
+ "name": "projects_likes_project_user_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "project_id",
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.projects_models": {
+ "name": "projects_models",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "draft": {
+ "name": "draft",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scene_graph": {
+ "name": "scene_graph",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "projects_models_project_id_projects_id_fk": {
+ "name": "projects_models_project_id_projects_id_fk",
+ "tableFrom": "projects_models",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.projects": {
+ "name": "projects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address_id": {
+ "name": "address_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details_json": {
+ "name": "details_json",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_private": {
+ "name": "is_private",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "show_scans_public": {
+ "name": "show_scans_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "show_guides_public": {
+ "name": "show_guides_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "likes": {
+ "name": "likes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "thumbnail_url": {
+ "name": "thumbnail_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "project_address_idx": {
+ "name": "project_address_idx",
+ "columns": [
+ {
+ "expression": "address_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "project_owner_idx": {
+ "name": "project_owner_idx",
+ "columns": [
+ {
+ "expression": "owner_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "project_is_private_idx": {
+ "name": "project_is_private_idx",
+ "columns": [
+ {
+ "expression": "is_private",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "project_views_idx": {
+ "name": "project_views_idx",
+ "columns": [
+ {
+ "expression": "views",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "project_likes_idx": {
+ "name": "project_likes_idx",
+ "columns": [
+ {
+ "expression": "likes",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "projects_address_id_projects_addresses_id_fk": {
+ "name": "projects_address_id_projects_addresses_id_fk",
+ "tableFrom": "projects",
+ "tableTo": "projects_addresses",
+ "columnsFrom": [
+ "address_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "projects_owner_id_auth_users_id_fk": {
+ "name": "projects_owner_id_auth_users_id_fk",
+ "tableFrom": "projects",
+ "tableTo": "auth_users",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ }
+ },
+ "enums": {
+ "public.auth_user_roles": {
+ "name": "auth_user_roles",
+ "schema": "public",
+ "values": [
+ "user",
+ "admin"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/supabase/migrations/meta/_journal.json b/supabase/migrations/meta/_journal.json
index 9921faab..8213f70a 100644
--- a/supabase/migrations/meta/_journal.json
+++ b/supabase/migrations/meta/_journal.json
@@ -36,6 +36,13 @@
"when": 1771911353115,
"tag": "20260224053553_stormy_carnage",
"breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "7",
+ "when": 1772065554845,
+ "tag": "20260226002554_big_pestilence",
+ "breakpoints": true
}
]
}
\ No newline at end of file