feat: Make galaxy viewable without login requirement

Implemented public galaxy viewing feature that allows unauthenticated
users to view public thought galaxies via the ?user={did} parameter,
while maintaining privacy controls for node-level visibility.

Changes:
- Updated /api/galaxy/route.ts to support public access:
  * Accept ?user={did} query parameter for viewing specific user's galaxy
  * Show all nodes (including private) for authenticated user viewing own galaxy
  * Filter to only public nodes when viewing someone else's galaxy
  * Return empty state with helpful message when not authenticated
  * Filter links to only show connections between visible nodes

- Added is_public field to database schema:
  * Updated db/schema.surql with DEFAULT true (public by default)
  * Created migration script scripts/add-is-public-field.ts
  * Aligns with ATproto's public-by-default philosophy

- Enhanced ThoughtGalaxy component:
  * Support viewing galaxies via ?user={did} parameter
  * Display user info banner when viewing public galaxy
  * Show appropriate empty state messages based on context
  * Refetch data when user parameter changes

- Created comprehensive Magnitude tests:
  * Test public galaxy viewing without authentication
  * Verify private nodes are hidden from public view
  * Test own galaxy access requires authentication
  * Validate invalid user DID handling
  * Test user info display and navigation between galaxies

- Documented implementation plan in plans/10-public-galaxy-viewing.md

This implements the "public by default" model while allowing future
node-level privacy controls. All canonical data remains on the user's
ATproto PDS, with SurrealDB serving as a high-performance cache.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-10 00:36:16 +00:00
parent aa60098690
commit d656b06113
6 changed files with 592 additions and 21 deletions

View File

@@ -95,18 +95,25 @@ export function ThoughtGalaxy() {
const [nodes, setNodes] = useState<NodeData[]>([]);
const [links, setLinks] = useState<LinkData[]>([]);
const [selectedNode, setSelectedNode] = useState<NodeData | null>(null);
const [emptyMessage, setEmptyMessage] = useState<string | null>(null);
const cameraControlsRef = useRef<CameraControls>(null);
const hasFitCamera = useRef(false);
const hasFocusedNode = useRef<string | null>(null);
// Get selectedNodeId from URL query params
// Get query params
const selectedNodeId = searchParams.get('node');
const targetUserDid = searchParams.get('user'); // For viewing someone else's galaxy
// Fetch data from API on mount and poll for updates
useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/galaxy', {
// Build URL with optional user parameter
const url = targetUserDid
? `/api/galaxy?user=${encodeURIComponent(targetUserDid)}`
: '/api/galaxy';
const response = await fetch(url, {
credentials: 'include', // Include cookies for authentication
});
@@ -119,13 +126,17 @@ export function ThoughtGalaxy() {
if (data.message) {
console.log('[ThoughtGalaxy]', data.message);
setEmptyMessage(data.message);
// If calculating, poll again in 2 seconds
setTimeout(fetchData, 2000);
if (data.message.includes('calculating')) {
setTimeout(fetchData, 2000);
}
return;
}
setNodes(data.nodes || []);
setLinks(data.links || []);
setEmptyMessage(null);
console.log(`[ThoughtGalaxy] Loaded ${data.nodes?.length || 0} nodes and ${data.links?.length || 0} links`);
} catch (error) {
@@ -134,7 +145,7 @@ export function ThoughtGalaxy() {
}
fetchData();
}, []);
}, [targetUserDid]);
// Function to fit camera to all nodes
const fitCameraToNodes = () => {
@@ -286,17 +297,49 @@ export function ThoughtGalaxy() {
return (
<Stack align="center" justify="center" style={{ height: '100vh', width: '100vw' }}>
<MantineText size="lg" c="dimmed">
Create at least 3 nodes to visualize your thought galaxy
</MantineText>
<MantineText size="sm" c="dimmed">
Nodes with content will automatically generate embeddings and 3D coordinates
{emptyMessage || 'Create at least 3 nodes to visualize your thought galaxy'}
</MantineText>
{!emptyMessage && (
<MantineText size="sm" c="dimmed">
Nodes with content will automatically generate embeddings and 3D coordinates
</MantineText>
)}
{targetUserDid && (
<MantineText size="sm" c="dimmed" mt="xs">
Viewing galaxy for user: {targetUserDid}
</MantineText>
)}
</Stack>
);
}
return (
<>
{/* User info banner when viewing someone else's galaxy */}
{targetUserDid && (
<Box
style={{
position: 'absolute',
top: '10px',
right: '10px',
zIndex: 999,
maxWidth: '300px',
}}
>
<Paper p="sm" radius="md" withBorder shadow="md">
<MantineText size="sm" fw={600}>
Public Galaxy
</MantineText>
<MantineText size="xs" c="dimmed">
Viewing {nodes.length} public {nodes.length === 1 ? 'node' : 'nodes'}
</MantineText>
<MantineText size="xs" c="dimmed" style={{ wordBreak: 'break-all' }}>
{targetUserDid}
</MantineText>
</Paper>
</Box>
)}
{/* Floating content overlay for selected node */}
{selectedNode && (
<Box