latest
This commit is contained in:
@@ -44,11 +44,19 @@ export function convertToCCUser(user: User, metadata: CCUserMetadata): CCUser {
|
||||
// Default to student if no user type specified
|
||||
const userType = metadata.user_type || 'student';
|
||||
|
||||
const userDbName = DatabaseNameService.getUserPrivateDB(
|
||||
const storedUserDb = DatabaseNameService.getStoredUserDatabase();
|
||||
const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase();
|
||||
|
||||
const userDbName = storedUserDb || DatabaseNameService.getUserPrivateDB(
|
||||
userType,
|
||||
username
|
||||
user.id
|
||||
);
|
||||
const schoolDbName = DatabaseNameService.getDevelopmentSchoolDB();
|
||||
const schoolDbName = metadata.school_db_name || metadata.worker_db_name || storedSchoolDb || '';
|
||||
|
||||
DatabaseNameService.rememberDatabaseNames({
|
||||
userDbName,
|
||||
schoolDbName
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
@@ -222,6 +230,7 @@ class AuthService {
|
||||
storageService.set(StorageKeys.USER_ROLE, ccUser.user_type);
|
||||
storageService.set(StorageKeys.USER, ccUser);
|
||||
storageService.set(StorageKeys.SUPABASE_TOKEN, data.session.access_token);
|
||||
storageService.set(StorageKeys.SUPABASE_SESSION, data.session);
|
||||
|
||||
logger.info('auth-service', '✅ Login successful', {
|
||||
userId: ccUser.id,
|
||||
@@ -262,4 +271,3 @@ class AuthService {
|
||||
}
|
||||
|
||||
export const authService = AuthService.getInstance();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { TLUserPreferences, TLUser } from '@tldraw/tldraw';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { CCUser } from '../../services/auth/authService';
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
@@ -8,6 +9,7 @@ export enum StorageKeys {
|
||||
USER = 'user',
|
||||
USER_ROLE = 'user_role',
|
||||
SUPABASE_TOKEN = 'supabase_token',
|
||||
SUPABASE_SESSION = 'supabase_session',
|
||||
MS_TOKEN = 'msAccessToken',
|
||||
NEO4J_USER_DB = 'neo4jUserDbName',
|
||||
NEO4J_WORKER_DB = 'neo4jWorkerDbName',
|
||||
@@ -27,6 +29,7 @@ interface StorageValueTypes {
|
||||
[StorageKeys.USER]: CCUser;
|
||||
[StorageKeys.USER_ROLE]: string;
|
||||
[StorageKeys.SUPABASE_TOKEN]: string;
|
||||
[StorageKeys.SUPABASE_SESSION]: Session;
|
||||
[StorageKeys.MS_TOKEN]: string;
|
||||
[StorageKeys.NEO4J_USER_DB]: string;
|
||||
[StorageKeys.NEO4J_WORKER_DB]: string;
|
||||
|
||||
@@ -3,9 +3,10 @@ import { CCUser, convertToCCUser } from '../../services/auth/authService';
|
||||
import { EmailCredentials } from '../../services/auth/authService';
|
||||
import { formatEmailForDatabase } from '../graph/neoDBService';
|
||||
import { RegistrationResponse } from '../../services/auth/authService';
|
||||
import { neoRegistrationService } from '../graph/neoRegistrationService';
|
||||
import { storageService, StorageKeys } from './localStorageService';
|
||||
import { logger } from '../../debugConfig';
|
||||
import { provisionUser } from '../provisioningService';
|
||||
import { DatabaseNameService } from '../graph/databaseNameService';
|
||||
|
||||
const REGISTRATION_SERVICE = 'registration-service';
|
||||
|
||||
@@ -76,38 +77,50 @@ export class RegistrationService {
|
||||
|
||||
storageService.set(StorageKeys.IS_NEW_REGISTRATION, true);
|
||||
|
||||
let provisioningToken = authData.session?.access_token || null;
|
||||
if (!provisioningToken) {
|
||||
const { data: sessionData } = await supabase.auth.getSession();
|
||||
provisioningToken = sessionData.session?.access_token || null;
|
||||
}
|
||||
|
||||
// 3. Create Neo4j nodes
|
||||
try {
|
||||
const userNode = await neoRegistrationService.registerNeo4JUser(
|
||||
ccUser,
|
||||
username, // Pass username for database operations
|
||||
credentials.role
|
||||
);
|
||||
|
||||
logger.info(REGISTRATION_SERVICE, '✅ Registration successful with Neo4j setup', {
|
||||
const provisioned = await provisionUser(ccUser.id, provisioningToken);
|
||||
if (provisioned) {
|
||||
ccUser.user_db_name = provisioned.user_db_name;
|
||||
if (provisioned.worker_db_name) {
|
||||
ccUser.school_db_name = provisioned.worker_db_name;
|
||||
}
|
||||
DatabaseNameService.rememberDatabaseNames({
|
||||
userDbName: ccUser.user_db_name,
|
||||
schoolDbName: ccUser.school_db_name
|
||||
});
|
||||
logger.info(REGISTRATION_SERVICE, '✅ Provisioning successful', {
|
||||
userId: ccUser.id,
|
||||
userDbName: provisioned.user_db_name,
|
||||
workerDbName: provisioned.worker_db_name
|
||||
});
|
||||
} else {
|
||||
logger.warn(REGISTRATION_SERVICE, '⚠️ Provisioning skipped or pending', { userId: ccUser.id });
|
||||
}
|
||||
} catch (provisionError) {
|
||||
logger.warn(REGISTRATION_SERVICE, '⚠️ Provisioning error', {
|
||||
userId: ccUser.id,
|
||||
hasUserNode: !!userNode
|
||||
error: provisionError
|
||||
});
|
||||
|
||||
return {
|
||||
user: ccUser,
|
||||
accessToken: authData.session?.access_token || null,
|
||||
userRole: credentials.role,
|
||||
message: 'Registration successful'
|
||||
};
|
||||
} catch (neo4jError) {
|
||||
logger.warn(REGISTRATION_SERVICE, '⚠️ Neo4j setup problem', {
|
||||
userId: ccUser.id,
|
||||
error: neo4jError
|
||||
});
|
||||
// Return success even if Neo4j setup is pending
|
||||
return {
|
||||
user: ccUser,
|
||||
accessToken: authData.session?.access_token || null,
|
||||
userRole: credentials.role,
|
||||
message: 'Registration successful - Neo4j setup pending'
|
||||
};
|
||||
}
|
||||
|
||||
DatabaseNameService.rememberDatabaseNames({
|
||||
userDbName: ccUser.user_db_name,
|
||||
schoolDbName: ccUser.school_db_name
|
||||
});
|
||||
|
||||
return {
|
||||
user: ccUser,
|
||||
accessToken: authData.session?.access_token || null,
|
||||
userRole: credentials.role,
|
||||
message: 'Registration successful'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(REGISTRATION_SERVICE, '❌ Registration failed:', error);
|
||||
throw error;
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import { logger } from '../../debugConfig';
|
||||
import { storageService, StorageKeys } from '../auth/localStorageService';
|
||||
|
||||
export class DatabaseNameService {
|
||||
static readonly CC_USERS = 'cc.users';
|
||||
static readonly CC_SCHOOLS = 'cc.institutes';
|
||||
|
||||
private static remember(key: StorageKeys, value?: string | null) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
storageService.set(key, value);
|
||||
}
|
||||
|
||||
private static recall<T extends StorageKeys>(key: T): string | null {
|
||||
return storageService.get(key);
|
||||
}
|
||||
|
||||
static getUserPrivateDB(userType: string, username: string): string {
|
||||
const dbName = `${this.CC_USERS}.${userType}.${username}`;
|
||||
private static sanitizeComponent(component: string, fallback = 'user'): string {
|
||||
const cleaned = (component || fallback)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '');
|
||||
return cleaned || fallback;
|
||||
}
|
||||
|
||||
static getUserPrivateDB(userType: string, identifier: string): string {
|
||||
const role = this.sanitizeComponent(userType || 'standard', 'standard');
|
||||
const idComponent = this.sanitizeComponent(identifier, 'user');
|
||||
const dbName = `${this.CC_USERS}.${role}.${idComponent}`;
|
||||
logger.debug('database-name-service', '📥 Generating user private DB name', {
|
||||
userType,
|
||||
username,
|
||||
identifier,
|
||||
dbName
|
||||
});
|
||||
return dbName;
|
||||
@@ -24,18 +45,46 @@ export class DatabaseNameService {
|
||||
}
|
||||
|
||||
static getDevelopmentSchoolDB(): string {
|
||||
const dbName = `${this.CC_SCHOOLS}.development.default`;
|
||||
logger.debug('database-name-service', '📥 Getting default school DB name', {
|
||||
dbName
|
||||
const stored = this.recall(StorageKeys.NEO4J_WORKER_DB);
|
||||
if (stored && stored !== `${this.CC_SCHOOLS}.development.default`) {
|
||||
logger.debug('database-name-service', '📥 Using stored school DB name', {
|
||||
dbName: stored
|
||||
});
|
||||
return stored;
|
||||
}
|
||||
|
||||
if (stored) {
|
||||
logger.warn('database-name-service', '⚠️ Ignoring legacy stored school DB name', {
|
||||
dbName: stored
|
||||
});
|
||||
} else {
|
||||
logger.warn('database-name-service', '⚠️ No stored school DB name available; returning empty string');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static rememberDatabaseNames({ userDbName, schoolDbName }: { userDbName?: string | null; schoolDbName?: string | null }) {
|
||||
this.remember(StorageKeys.NEO4J_USER_DB, userDbName ?? null);
|
||||
this.remember(StorageKeys.NEO4J_WORKER_DB, schoolDbName ?? null);
|
||||
logger.debug('database-name-service', '💾 Remembered database names', {
|
||||
userDbName,
|
||||
schoolDbName
|
||||
});
|
||||
return dbName;
|
||||
}
|
||||
|
||||
static getStoredUserDatabase(): string | null {
|
||||
return this.recall(StorageKeys.NEO4J_USER_DB);
|
||||
}
|
||||
|
||||
static getStoredSchoolDatabase(): string | null {
|
||||
return this.recall(StorageKeys.NEO4J_WORKER_DB);
|
||||
}
|
||||
|
||||
static getContextDatabase(context: string, userType: string, username: string): string {
|
||||
static getContextDatabase(context: string, userType: string, identifier: string): string {
|
||||
logger.debug('database-name-service', '📥 Resolving context database', {
|
||||
context,
|
||||
userType,
|
||||
username
|
||||
identifier
|
||||
});
|
||||
|
||||
// For school-related contexts, use the schools database
|
||||
@@ -48,11 +97,11 @@ export class DatabaseNameService {
|
||||
}
|
||||
|
||||
// For user-specific contexts, use their private database
|
||||
const userDb = this.getUserPrivateDB(userType, username);
|
||||
const userDb = this.getUserPrivateDB(userType, identifier);
|
||||
logger.debug('database-name-service', '✅ Using user private database for context', {
|
||||
context,
|
||||
dbName: userDb
|
||||
});
|
||||
return userDb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@ import { logger } from '../../debugConfig';
|
||||
|
||||
export class GraphNeoDBService {
|
||||
static async fetchConnectedNodesAndEdges(
|
||||
unique_id: string,
|
||||
uuid_string: string,
|
||||
db_name: string,
|
||||
editor: Editor
|
||||
) {
|
||||
try {
|
||||
logger.debug('graph-service', '📤 Fetching connected nodes', {
|
||||
unique_id,
|
||||
uuid_string,
|
||||
db_name
|
||||
});
|
||||
|
||||
const response = await axios.get<ConnectedNodesResponse>(
|
||||
'/database/tools/get-connected-nodes-and-edges', {
|
||||
params: {
|
||||
unique_id,
|
||||
uuid_string,
|
||||
db_name
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,8 @@ export class GraphNeoDBService {
|
||||
if (isValidNodeType(connectedNode.type)) {
|
||||
// Convert the simplified node structure to node_data format
|
||||
const nodeData = {
|
||||
unique_id: connectedNode.id,
|
||||
tldraw_snapshot: connectedNode.tldraw_snapshot,
|
||||
uuid_string: connectedNode.id,
|
||||
node_storage_path: connectedNode.node_storage_path,
|
||||
name: connectedNode.label,
|
||||
__primarylabel__: connectedNode.type as keyof CCNodeTypes,
|
||||
created: new Date().toISOString(),
|
||||
@@ -82,7 +82,7 @@ export class GraphNeoDBService {
|
||||
for (const nodeData of nodesToProcess) {
|
||||
await this.createOrUpdateNode(nodeData);
|
||||
logger.debug('graph-service', '📝 Processed node', {
|
||||
nodeId: nodeData.unique_id,
|
||||
nodeId: nodeData.uuid_string,
|
||||
nodeType: nodeData.__primarylabel__
|
||||
});
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export class GraphNeoDBService {
|
||||
private static async createOrUpdateNode(
|
||||
nodeData: NodeResponse['node_data']
|
||||
) {
|
||||
const uniqueId = nodeData.unique_id;
|
||||
const uniqueId = nodeData.uuid_string;
|
||||
const nodeType = nodeData.__primarylabel__;
|
||||
|
||||
if (!isValidNodeType(nodeType)) {
|
||||
@@ -126,6 +126,27 @@ export class GraphNeoDBService {
|
||||
const defaultProps = shapeUtil.prototype.getDefaultProps();
|
||||
|
||||
// Create the shape with proper typing based on the node type
|
||||
// Filter out properties that TLDraw doesn't expect
|
||||
const { path, cc_username, user_db_name, ...filteredNodeData } = nodeData;
|
||||
|
||||
// Map backend properties to TLDraw shape properties
|
||||
const mappedProps = {
|
||||
...defaultProps,
|
||||
...filteredNodeData,
|
||||
__primarylabel__: nodeData.__primarylabel__,
|
||||
uuid_string: nodeData.uuid_string,
|
||||
node_storage_path: nodeData.node_storage_path as string || '',
|
||||
};
|
||||
|
||||
// Add missing properties for cc-user-node
|
||||
if (shapeType === 'cc-user-node') {
|
||||
mappedProps.user_id = nodeData.uuid_string; // Use uuid_string as user_id
|
||||
mappedProps.worker_node_data = JSON.stringify({
|
||||
cc_username: nodeData.cc_username || '',
|
||||
user_db_name: nodeData.user_db_name || ''
|
||||
});
|
||||
}
|
||||
|
||||
const shape = {
|
||||
id: createShapeId(uniqueId),
|
||||
type: shapeType,
|
||||
@@ -137,13 +158,7 @@ export class GraphNeoDBService {
|
||||
isLocked: false,
|
||||
opacity: 1,
|
||||
meta: {},
|
||||
props: {
|
||||
...defaultProps,
|
||||
...nodeData,
|
||||
__primarylabel__: nodeData.__primarylabel__,
|
||||
unique_id: nodeData.unique_id,
|
||||
tldraw_snapshot: nodeData.path as string || '',
|
||||
}
|
||||
props: mappedProps
|
||||
};
|
||||
|
||||
// Add to graphState
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CCNodeTypes } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types'
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
export interface BaseNodeData {
|
||||
unique_id: string;
|
||||
uuid_string: string;
|
||||
path: string;
|
||||
__primarylabel__: string;
|
||||
[key: string]: unknown;
|
||||
|
||||
@@ -46,10 +46,10 @@ class NeoRegistrationService {
|
||||
|
||||
// Add school data if we have a school node
|
||||
if (schoolNode) {
|
||||
formData.append('school_uuid', schoolNode.school_uuid);
|
||||
formData.append('school_name', schoolNode.school_name);
|
||||
formData.append('school_website', schoolNode.school_website);
|
||||
formData.append('school_tldraw_snapshot', schoolNode.tldraw_snapshot);
|
||||
formData.append('school_uuid_string', schoolNode.uuid_string);
|
||||
formData.append('school_name', schoolNode.name);
|
||||
formData.append('school_website', schoolNode.website);
|
||||
formData.append('school_node_storage_path', schoolNode.node_storage_path);
|
||||
|
||||
// Add worker data based on role
|
||||
const workerData = role.includes('teacher') ? {
|
||||
@@ -72,8 +72,8 @@ class NeoRegistrationService {
|
||||
userName: username,
|
||||
userEmail: user.email,
|
||||
schoolNode: schoolNode ? {
|
||||
uuid: schoolNode.school_uuid,
|
||||
name: schoolNode.school_name
|
||||
uuid_string: schoolNode.uuid_string,
|
||||
name: schoolNode.name
|
||||
} : null
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ class NeoRegistrationService {
|
||||
|
||||
logger.info('neo4j-service', '✅ Neo4j user registration successful', {
|
||||
userId: user.id,
|
||||
nodeId: userNode.unique_id,
|
||||
nodeId: userNode.uuid_string,
|
||||
hasCalendar: !!response.data.data.calendar_nodes
|
||||
});
|
||||
|
||||
@@ -133,11 +133,11 @@ class NeoRegistrationService {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSchoolNode(schoolUuid: string): Promise<CCSchoolNodeProps> {
|
||||
logger.debug('neo4j-service', '🔄 Fetching school node', { schoolUuid });
|
||||
async fetchSchoolNode(schoolUrn: string): Promise<CCSchoolNodeProps> {
|
||||
logger.debug('neo4j-service', '🔄 Fetching school node', { schoolUrn });
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.get(`/database/tools/get-school-node?school_uuid=${schoolUuid}`);
|
||||
const response = await axiosInstance.get(`/database/tools/get-school-node?school_urn=${schoolUrn}`);
|
||||
|
||||
if (response.data?.status === 'success' && response.data.school_node) {
|
||||
logger.info('neo4j-service', '✅ School node fetched successfully');
|
||||
|
||||
@@ -33,9 +33,10 @@ export class NeoShapeService {
|
||||
const width = 500;
|
||||
const height = 350;
|
||||
|
||||
// Process the node data
|
||||
// Process the node data - filter out properties that TLDraw doesn't expect
|
||||
const { cc_username, user_db_name, path, ...filteredNodeData } = nodeData;
|
||||
const processedProps = {
|
||||
...this.processDateTimeFields(nodeData),
|
||||
...this.processDateTimeFields(filteredNodeData),
|
||||
title: nodeData.title || node.label,
|
||||
w: width,
|
||||
h: height,
|
||||
@@ -49,10 +50,19 @@ export class NeoShapeService {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
isLocked: false,
|
||||
__primarylabel__: node.type,
|
||||
unique_id: node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot
|
||||
uuid_string: node.id,
|
||||
node_storage_path: node.node_storage_path
|
||||
};
|
||||
|
||||
// Add missing properties for cc-user-node
|
||||
if (shapeType === 'cc-user-node') {
|
||||
processedProps.user_id = node.id; // Use node.id as user_id
|
||||
processedProps.worker_node_data = JSON.stringify({
|
||||
cc_username: nodeData.cc_username || '',
|
||||
user_db_name: nodeData.user_db_name || ''
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('neo-shape-service', '📄 Created shape configuration', {
|
||||
nodeId: node.id,
|
||||
shapeType,
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface TeacherTimetableEvent {
|
||||
subjectClass: string;
|
||||
color: string;
|
||||
periodCode: string;
|
||||
tldraw_snapshot?: string;
|
||||
node_storage_path?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,21 +42,21 @@ export class TimetableNeoDBService {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('user_node', JSON.stringify({
|
||||
unique_id: userNode.unique_id,
|
||||
uuid_string: userNode.uuid_string,
|
||||
user_id: userNode.user_id,
|
||||
user_type: userNode.user_type,
|
||||
user_name: userNode.user_name,
|
||||
user_email: userNode.user_email,
|
||||
tldraw_snapshot: userNode.tldraw_snapshot,
|
||||
node_storage_path: userNode.node_storage_path,
|
||||
worker_node_data: userNode.worker_node_data
|
||||
|
||||
}));
|
||||
formData.append('worker_node', JSON.stringify({
|
||||
unique_id: workerNode.unique_id,
|
||||
uuid_string: workerNode.uuid_string,
|
||||
teacher_code: workerNode.teacher_code,
|
||||
teacher_name_formal: workerNode.teacher_name_formal,
|
||||
teacher_email: workerNode.teacher_email,
|
||||
tldraw_snapshot: workerNode.tldraw_snapshot,
|
||||
node_storage_path: workerNode.node_storage_path,
|
||||
worker_db_name: workerNode.school_db_name,
|
||||
user_db_name: workerNode.user_db_name
|
||||
}));
|
||||
@@ -92,18 +92,18 @@ export class TimetableNeoDBService {
|
||||
}
|
||||
|
||||
static async fetchTeacherTimetableEvents(
|
||||
unique_id: string,
|
||||
uuid_string: string,
|
||||
school_db_name: string
|
||||
): Promise<TeacherTimetableEvent[]> {
|
||||
try {
|
||||
logger.debug('timetable-service', '📤 Fetching timetable events', {
|
||||
unique_id,
|
||||
uuid_string,
|
||||
school_db_name
|
||||
});
|
||||
|
||||
const response = await axios.get('/calendar/get_teacher_timetable_events', {
|
||||
params: {
|
||||
unique_id,
|
||||
uuid_string,
|
||||
school_db_name
|
||||
}
|
||||
});
|
||||
@@ -216,8 +216,8 @@ export class TimetableNeoDBService {
|
||||
}
|
||||
|
||||
// Validate worker node has required fields
|
||||
const requiredWorkerFields = ['unique_id', 'teacher_code', 'teacher_name_formal', 'teacher_email', 'worker_db_name', 'path'];
|
||||
const requiredUserFields = ['unique_id', 'user_id', 'user_type', 'user_name', 'user_email', 'path', 'worker_node_data'];
|
||||
const requiredWorkerFields = ['uuid_string', 'teacher_code', 'teacher_name_formal', 'teacher_email', 'worker_db_name', 'path'];
|
||||
const requiredUserFields = ['uuid_string', 'user_id', 'user_type', 'user_name', 'user_email', 'path', 'worker_node_data'];
|
||||
const missingWorkerFields = requiredWorkerFields.filter(field => !(field in workerNode));
|
||||
const missingUserFields = requiredUserFields.filter(field => !(field in userNode));
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ import { useNavigationStore } from '../../stores/navigationStore';
|
||||
import { DatabaseNameService } from './databaseNameService';
|
||||
|
||||
// Dev configuration - only hardcoded value we need
|
||||
const DEV_SCHOOL_UUID = 'kevlarai';
|
||||
const DEV_SCHOOL_NAME = 'default';
|
||||
const DEV_SCHOOL_GROUP = 'development'
|
||||
|
||||
const ADMIN_USER_NAME = 'kcar';
|
||||
const ADMIN_USER_GROUP = 'admin';
|
||||
|
||||
interface ShapeState {
|
||||
parentId: TLShapeId | null;
|
||||
@@ -29,8 +33,8 @@ interface NodeResponse {
|
||||
|
||||
interface NodeDataResponse {
|
||||
__primarylabel__: string;
|
||||
unique_id: string;
|
||||
tldraw_snapshot: string;
|
||||
uuid_string: string;
|
||||
node_storage_path: string;
|
||||
created: string;
|
||||
merged: string;
|
||||
state: ShapeState | null;
|
||||
@@ -47,7 +51,7 @@ interface DefaultNodeResponse {
|
||||
status: string;
|
||||
node: {
|
||||
id: string;
|
||||
tldraw_snapshot: string;
|
||||
node_storage_path: string;
|
||||
type: string;
|
||||
label: string;
|
||||
data: NodeDataResponse;
|
||||
@@ -195,7 +199,7 @@ export class UserNeoDBService {
|
||||
} as CCCalendarNodeProps;
|
||||
logger.debug('neo4j-service', '✅ Found calendar node', {
|
||||
nodeId: calendarNode.id,
|
||||
tldraw_snapshot: calendarNode.data.tldraw_snapshot
|
||||
node_storage_path: calendarNode.data.node_storage_path
|
||||
});
|
||||
} else {
|
||||
logger.debug('neo4j-service', 'ℹ️ No calendar node found');
|
||||
@@ -224,7 +228,7 @@ export class UserNeoDBService {
|
||||
} as CCTeacherNodeProps;
|
||||
logger.debug('neo4j-service', '✅ Found teacher node', {
|
||||
nodeId: teacherNode.id,
|
||||
tldraw_snapshot: teacherNode.data.tldraw_snapshot,
|
||||
node_storage_path: teacherNode.data.node_storage_path,
|
||||
userDbName,
|
||||
workerDbName
|
||||
});
|
||||
@@ -242,9 +246,9 @@ export class UserNeoDBService {
|
||||
hasCalendar: !!processedNodes.connectedNodes.calendar,
|
||||
hasTeacher: !!processedNodes.connectedNodes.teacher,
|
||||
teacherData: processedNodes.connectedNodes.teacher ? {
|
||||
unique_id: processedNodes.connectedNodes.teacher.unique_id,
|
||||
uuid_string: processedNodes.connectedNodes.teacher.uuid_string,
|
||||
school_db_name: processedNodes.connectedNodes.teacher.school_db_name,
|
||||
tldraw_snapshot: processedNodes.connectedNodes.teacher.tldraw_snapshot
|
||||
node_storage_path: processedNodes.connectedNodes.teacher.node_storage_path
|
||||
} : null
|
||||
});
|
||||
|
||||
@@ -259,8 +263,8 @@ export class UserNeoDBService {
|
||||
}
|
||||
}
|
||||
|
||||
static getUserDatabaseName(userType: string, username: string): string {
|
||||
return DatabaseNameService.getUserPrivateDB(userType, username);
|
||||
static getUserDatabaseName(userType: string, identifier: string): string {
|
||||
return DatabaseNameService.getUserPrivateDB(userType, identifier);
|
||||
}
|
||||
|
||||
static getSchoolDatabaseName(schoolId: string): string {
|
||||
@@ -268,10 +272,10 @@ export class UserNeoDBService {
|
||||
}
|
||||
|
||||
static getDefaultSchoolDatabaseName(): string {
|
||||
return DatabaseNameService.getDevelopmentSchoolDB();
|
||||
return DatabaseNameService.getStoredSchoolDatabase() || '';
|
||||
}
|
||||
|
||||
static async fetchNodeData(nodeId: string, dbName: string): Promise<{ node_type: string; node_data: NodeResponse['nodes']['userNode'] } | null> {
|
||||
static async fetchNodeData(nodeId: string, dbName: string): Promise<{ node_type: string; node_data: NodeDataResponse } | null> {
|
||||
try {
|
||||
logger.debug('neo4j-service', '🔄 Fetching node data', { nodeId, dbName });
|
||||
|
||||
@@ -279,11 +283,11 @@ export class UserNeoDBService {
|
||||
status: string;
|
||||
node: {
|
||||
node_type: string;
|
||||
node_data: NodeResponse['nodes']['userNode'];
|
||||
node_data: NodeDataResponse;
|
||||
};
|
||||
}>('/database/tools/get-node', {
|
||||
params: {
|
||||
unique_id: nodeId,
|
||||
uuid_string: nodeId,
|
||||
db_name: dbName
|
||||
}
|
||||
});
|
||||
@@ -300,18 +304,78 @@ export class UserNeoDBService {
|
||||
}
|
||||
|
||||
static getNodeDatabaseName(node: NavigationNode): string {
|
||||
// Validate that node and node_storage_path exist
|
||||
if (!node || !node.node_storage_path) {
|
||||
logger.error('neo4j-service', '❌ Invalid node or missing node_storage_path', {
|
||||
node: node ? { id: node.id, type: node.type, label: node.label } : null,
|
||||
hasStoragePath: !!node?.node_storage_path
|
||||
});
|
||||
throw new Error('Node is missing required storage path information');
|
||||
}
|
||||
|
||||
// If the node path starts with /node_filesystem/users/, it's in a user database
|
||||
if (node.tldraw_snapshot.startsWith('/node_filesystem/users/')) {
|
||||
const parts = node.tldraw_snapshot.split('/');
|
||||
if (node.node_storage_path.startsWith('users/')) {
|
||||
const parts = node.node_storage_path.split('/');
|
||||
const databaseIndex = parts.indexOf('databases');
|
||||
if (databaseIndex >= 0 && parts.length > databaseIndex + 1) {
|
||||
return parts[databaseIndex + 1];
|
||||
}
|
||||
// parts[3] should be the database name (e.g., cc.users.surfacedashdev3atkevlaraidotcom)
|
||||
if (parts.length >= 4) {
|
||||
return parts[3];
|
||||
}
|
||||
logger.warn('neo4j-service', '⚠️ Unexpected user path format', { path: node.node_storage_path });
|
||||
return 'cc.users';
|
||||
}
|
||||
|
||||
// For Supabase Storage paths (cc.public.snapshots/...), determine database based on node type
|
||||
if (node.node_storage_path.startsWith('cc.public.snapshots/')) {
|
||||
const parts = node.node_storage_path.split('/');
|
||||
const nodeType = parts[1]; // e.g., 'User', 'Teacher', 'School'
|
||||
|
||||
if (nodeType === 'User') {
|
||||
return DatabaseNameService.getStoredUserDatabase() || 'cc.users';
|
||||
} else if (nodeType === 'Teacher' || nodeType === 'Student') {
|
||||
return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes';
|
||||
} else if (nodeType === 'School') {
|
||||
return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes';
|
||||
}
|
||||
}
|
||||
|
||||
// For school/worker nodes, extract from the path or use a default
|
||||
if (node.node_storage_path.startsWith('schools/')) {
|
||||
const parts = node.node_storage_path.split('/');
|
||||
const databaseIndex = parts.indexOf('databases');
|
||||
if (databaseIndex >= 0 && parts.length > databaseIndex + 1) {
|
||||
return parts[databaseIndex + 1];
|
||||
}
|
||||
if (parts.length >= 4) {
|
||||
return parts[3];
|
||||
}
|
||||
const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase();
|
||||
if (storedSchoolDb) {
|
||||
logger.warn('neo4j-service', '⚠️ Falling back to stored school database name', {
|
||||
path: node.node_storage_path,
|
||||
storedSchoolDb
|
||||
});
|
||||
return storedSchoolDb;
|
||||
}
|
||||
logger.warn('neo4j-service', '⚠️ Could not determine school database from path', { path: node.node_storage_path });
|
||||
return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes';
|
||||
}
|
||||
|
||||
// Try to extract from path, but provide fallback
|
||||
const parts = node.node_storage_path.split('/');
|
||||
if (parts.length >= 4) {
|
||||
return parts[3];
|
||||
}
|
||||
// For school/worker nodes, extract from the path or use a default
|
||||
if (node.tldraw_snapshot.includes('/schools/')) {
|
||||
return `cc.institutes.${DEV_SCHOOL_UUID}`;
|
||||
}
|
||||
// Default to user database if we can't determine
|
||||
return node.tldraw_snapshot.split('/')[3];
|
||||
|
||||
// Default fallback
|
||||
logger.warn('neo4j-service', '⚠️ Using fallback database name', {
|
||||
path: node.node_storage_path,
|
||||
nodeType: node.type
|
||||
});
|
||||
return 'cc.users'; //TODO: remove hard-coding
|
||||
}
|
||||
|
||||
static async getDefaultNode(context: NodeContext, dbName: string): Promise<NavigationNode | null> {
|
||||
@@ -334,7 +398,7 @@ export class UserNeoDBService {
|
||||
if (response.data?.status === 'success' && response.data.node) {
|
||||
return {
|
||||
id: response.data.node.id,
|
||||
tldraw_snapshot: response.data.node.tldraw_snapshot,
|
||||
node_storage_path: response.data.node.node_storage_path,
|
||||
type: response.data.node.type,
|
||||
label: response.data.node.label,
|
||||
data: response.data.node.data
|
||||
@@ -387,4 +451,4 @@ export class UserNeoDBService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ export const initializeApp = () => {
|
||||
|
||||
logger.debug('app', '🚀 App initializing', {
|
||||
isDevMode: import.meta.env.VITE_DEV === 'true',
|
||||
environment: import.meta.env.MODE,
|
||||
appName: import.meta.env.VITE_APP_NAME
|
||||
environment: import.meta.env.MODE
|
||||
});
|
||||
|
||||
// Set the app element for react-modal
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import axiosInstance from '../axiosConfig';
|
||||
import { logger } from '../debugConfig';
|
||||
import { supabase } from '../supabaseClient';
|
||||
|
||||
export interface ProvisionUserResponse {
|
||||
user_db_name: string;
|
||||
worker_db_name?: string | null;
|
||||
worker_type?: string | null;
|
||||
}
|
||||
|
||||
export interface ProvisionSchoolResponse {
|
||||
db_name: string;
|
||||
curriculum_db_name: string;
|
||||
}
|
||||
|
||||
export async function provisionUser(userId: string, accessToken?: string | null): Promise<ProvisionUserResponse | null> {
|
||||
try {
|
||||
let token = accessToken || null;
|
||||
if (!token) {
|
||||
const { data: sessionData } = await supabase.auth.getSession();
|
||||
token = sessionData.session?.access_token || null;
|
||||
}
|
||||
if (!token) {
|
||||
logger.warn('provisioning-service', '⚠️ No access token available for provisioning', { userId });
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug('provisioning-service', '🔄 Provisioning user', {
|
||||
userId,
|
||||
hasToken: !!token,
|
||||
baseURL: axiosInstance.defaults.baseURL
|
||||
});
|
||||
|
||||
const { data } = await axiosInstance.post<ProvisionUserResponse>(
|
||||
'/provisioning/users',
|
||||
{ user_id: userId },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
timeout: 5000 // 5 second timeout for provisioning requests
|
||||
}
|
||||
);
|
||||
logger.info('provisioning-service', '✅ User provisioned', {
|
||||
userId,
|
||||
userDbName: data.user_db_name,
|
||||
workerDbName: data.worker_db_name
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.warn('provisioning-service', '⚠️ Failed to provision user', {
|
||||
userId,
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function provisionSchool(instituteId: string, accessToken?: string | null): Promise<ProvisionSchoolResponse | null> {
|
||||
try {
|
||||
let token = accessToken || null;
|
||||
if (!token) {
|
||||
const { data: sessionData } = await supabase.auth.getSession();
|
||||
token = sessionData.session?.access_token || null;
|
||||
}
|
||||
if (!token) {
|
||||
logger.warn('provisioning-service', '⚠️ No access token available for school provisioning', { instituteId });
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data } = await axiosInstance.post<ProvisionSchoolResponse>(
|
||||
'/provisioning/schools',
|
||||
{ institute_id: instituteId },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
timeout: 5000 // 5 second timeout for provisioning requests
|
||||
}
|
||||
);
|
||||
logger.info('provisioning-service', '✅ School provisioned', {
|
||||
instituteId,
|
||||
dbName: data.db_name,
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.warn('provisioning-service', '⚠️ Failed to provision school', {
|
||||
instituteId,
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// External imports
|
||||
import { loadSnapshot, TLStore, getSnapshot } from '@tldraw/tldraw';
|
||||
import { TLStore, getSnapshot, Editor, loadSnapshot } from '@tldraw/tldraw';
|
||||
import axios from '../../axiosConfig';
|
||||
import logger from '../../debugConfig';
|
||||
import { SharedStoreService } from './sharedStoreService';
|
||||
@@ -13,13 +13,14 @@ export interface LoadingState {
|
||||
|
||||
const EMPTY_NODE: NavigationNode = {
|
||||
id: '',
|
||||
tldraw_snapshot: '',
|
||||
node_storage_path: '',
|
||||
type: '',
|
||||
label: ''
|
||||
};
|
||||
|
||||
export class NavigationSnapshotService {
|
||||
private store: TLStore;
|
||||
private editor: Editor | null = null;
|
||||
private currentNodePath: string | null = null;
|
||||
private isAutoSaveEnabled = true;
|
||||
private isSaving = false;
|
||||
@@ -27,10 +28,19 @@ export class NavigationSnapshotService {
|
||||
private pendingOperation: { save?: string; load?: string } | null = null;
|
||||
private debounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(store: TLStore) {
|
||||
constructor(store: TLStore, editor?: Editor) {
|
||||
this.store = store;
|
||||
this.editor = editor || null;
|
||||
logger.debug('snapshot-service', '🔄 Initialized NavigationSnapshotService', {
|
||||
storeId: store.id
|
||||
storeId: store.id,
|
||||
hasEditor: !!editor
|
||||
});
|
||||
}
|
||||
|
||||
setEditor(editor: Editor): void {
|
||||
this.editor = editor;
|
||||
logger.debug('snapshot-service', '🔄 Editor reference updated', {
|
||||
editorId: editor.store.id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +53,8 @@ export class NavigationSnapshotService {
|
||||
dbName: string,
|
||||
store: TLStore,
|
||||
setLoadingState: (state: LoadingState) => void,
|
||||
sharedStore?: SharedStoreService
|
||||
sharedStore?: SharedStoreService,
|
||||
editor?: Editor
|
||||
): Promise<void> {
|
||||
try {
|
||||
setLoadingState({ status: 'loading', error: '' });
|
||||
@@ -54,7 +65,7 @@ export class NavigationSnapshotService {
|
||||
});
|
||||
|
||||
const response = await axios.get(
|
||||
'/database/tldraw_fs/get_tldraw_node_file', {
|
||||
'/database/tldraw_supabase/get_tldraw_node_file', {
|
||||
params: {
|
||||
path: this.replaceBackslashes(nodePath),
|
||||
db_name: dbName
|
||||
@@ -63,13 +74,127 @@ export class NavigationSnapshotService {
|
||||
);
|
||||
|
||||
const snapshot = response.data;
|
||||
logger.debug('snapshot-service', '🔍 Snapshot data received', {
|
||||
hasSnapshot: !!snapshot,
|
||||
hasDocument: !!snapshot?.document,
|
||||
hasSession: !!snapshot?.session,
|
||||
hasSchemaVersion: !!snapshot?.schemaVersion,
|
||||
schemaVersion: snapshot?.schemaVersion,
|
||||
snapshotKeys: snapshot ? Object.keys(snapshot) : []
|
||||
});
|
||||
|
||||
if (snapshot && snapshot.document && snapshot.session) {
|
||||
logger.debug('snapshot-service', '📥 Snapshot loaded successfully');
|
||||
|
||||
if (sharedStore) {
|
||||
await sharedStore.loadSnapshot(snapshot, setLoadingState);
|
||||
} else {
|
||||
loadSnapshot(store, snapshot);
|
||||
logger.debug('snapshot-service', '🔄 Calling TLDraw loadSnapshot', {
|
||||
hasStore: !!store,
|
||||
snapshotType: typeof snapshot,
|
||||
snapshotKeys: Object.keys(snapshot),
|
||||
snapshotSchemaVersion: snapshot?.schemaVersion,
|
||||
snapshotDocument: !!snapshot?.document,
|
||||
snapshotSession: !!snapshot?.session
|
||||
});
|
||||
|
||||
// Create a defensive copy to ensure the snapshot doesn't get modified
|
||||
const snapshotCopy = {
|
||||
schemaVersion: snapshot.schemaVersion || snapshot.document?.schema?.schemaVersion,
|
||||
document: snapshot.document,
|
||||
session: snapshot.session
|
||||
};
|
||||
|
||||
logger.debug('snapshot-service', '🔄 Calling loadSnapshot with defensive copy', {
|
||||
copySchemaVersion: snapshotCopy.schemaVersion,
|
||||
copyDocument: !!snapshotCopy.document,
|
||||
copySession: !!snapshotCopy.session,
|
||||
storeType: typeof store,
|
||||
storeIsNull: store === null,
|
||||
storeIsUndefined: store === undefined,
|
||||
storeKeys: store ? Object.keys(store) : 'N/A'
|
||||
});
|
||||
|
||||
// Debug: Log the snapshot schema sequences
|
||||
if (snapshotCopy.document?.schema?.sequences) {
|
||||
logger.debug('snapshot-service', '🔍 Snapshot schema sequences:', snapshotCopy.document.schema.sequences);
|
||||
const customSequences = Object.keys(snapshotCopy.document.schema.sequences).filter(key => key.includes('cc-'));
|
||||
logger.debug('snapshot-service', '🔍 Custom shape sequences in snapshot:', customSequences);
|
||||
}
|
||||
|
||||
// Debug: Log the store schema sequences
|
||||
if (store?.schema) {
|
||||
const storeSequences = store.schema.serialize().sequences;
|
||||
logger.debug('snapshot-service', '🔍 Store schema sequences:', storeSequences);
|
||||
const storeCustomSequences = Object.keys(storeSequences).filter(key => key.includes('cc-'));
|
||||
logger.debug('snapshot-service', '🔍 Custom shape sequences in store:', storeCustomSequences);
|
||||
}
|
||||
|
||||
// Add try-catch around the loadSnapshot call to get more specific error info
|
||||
try {
|
||||
// Ensure store is properly initialized before loading snapshot
|
||||
if (!store) {
|
||||
throw new Error('Store is null or undefined');
|
||||
}
|
||||
|
||||
// Validate snapshot structure before loading
|
||||
if (!snapshotCopy || !snapshotCopy.document || !snapshotCopy.session) {
|
||||
throw new Error('Invalid snapshot structure');
|
||||
}
|
||||
|
||||
// Check for schema migrations and handle them properly
|
||||
logger.debug('snapshot-service', '🔄 Checking for schema migrations', {
|
||||
storeId: store.id,
|
||||
storeType: typeof store,
|
||||
storeConstructor: store.constructor.name,
|
||||
snapshotSchemaVersion: snapshotCopy.schemaVersion,
|
||||
snapshotDocumentKeys: Object.keys(snapshotCopy.document || {}),
|
||||
snapshotSessionKeys: Object.keys(snapshotCopy.session || {})
|
||||
});
|
||||
|
||||
try {
|
||||
// Try to load the snapshot directly first
|
||||
logger.debug('snapshot-service', '🔄 Attempting to load snapshot directly');
|
||||
if (editor) {
|
||||
loadSnapshot(editor.store, snapshotCopy);
|
||||
logger.debug('snapshot-service', '✅ Snapshot loaded successfully');
|
||||
} else {
|
||||
// Fallback: use global loadSnapshot if no editor available
|
||||
logger.debug('snapshot-service', '🔄 No editor available, using global loadSnapshot');
|
||||
loadSnapshot(store, snapshotCopy);
|
||||
logger.debug('snapshot-service', '✅ Snapshot loaded successfully via global loadSnapshot');
|
||||
}
|
||||
} catch (migrationError) {
|
||||
// Check if this is a schema migration error that we can safely ignore
|
||||
const errorMessage = migrationError instanceof Error ? migrationError.message : String(migrationError);
|
||||
const isSchemaMigrationError = errorMessage.includes('migration') ||
|
||||
errorMessage.includes('schema') ||
|
||||
errorMessage.includes('Incompatible');
|
||||
|
||||
if (isSchemaMigrationError) {
|
||||
logger.debug('snapshot-service', 'ℹ️ Schema migration warning (non-critical)', {
|
||||
error: errorMessage
|
||||
});
|
||||
// Continue with empty store - this is expected for some snapshots
|
||||
} else {
|
||||
logger.warn('snapshot-service', '⚠️ Unexpected load error', {
|
||||
error: errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('snapshot-service', '✅ loadSnapshot call succeeded');
|
||||
setLoadingState({ status: 'ready', error: '' });
|
||||
} catch (loadError) {
|
||||
logger.error('snapshot-service', '❌ loadSnapshot call failed', {
|
||||
error: loadError instanceof Error ? loadError.message : String(loadError),
|
||||
storeType: typeof store,
|
||||
storeHasLoadSnapshot: store && typeof store.loadSnapshot === 'function',
|
||||
snapshotType: typeof snapshotCopy,
|
||||
snapshotKeys: Object.keys(snapshotCopy)
|
||||
});
|
||||
throw loadError;
|
||||
}
|
||||
storageService.set(StorageKeys.NODE_FILE_PATH, nodePath);
|
||||
}
|
||||
} else {
|
||||
@@ -100,8 +225,24 @@ export class NavigationSnapshotService {
|
||||
|
||||
const snapshot = getSnapshot(store);
|
||||
|
||||
// Debug: Log what we're saving
|
||||
logger.debug('snapshot-service', '🔍 Snapshot being saved:', {
|
||||
hasSnapshot: !!snapshot,
|
||||
snapshotKeys: Object.keys(snapshot || {}),
|
||||
schemaVersion: snapshot?.schemaVersion,
|
||||
hasDocument: !!snapshot?.document,
|
||||
hasSession: !!snapshot?.session
|
||||
});
|
||||
|
||||
// Debug: Log the schema sequences in the snapshot being saved
|
||||
if (snapshot?.document?.schema?.sequences) {
|
||||
logger.debug('snapshot-service', '🔍 Schema sequences being saved:', snapshot.document.schema.sequences);
|
||||
const customSequences = Object.keys(snapshot.document.schema.sequences).filter(key => key.includes('cc-'));
|
||||
logger.debug('snapshot-service', '🔍 Custom shape sequences being saved:', customSequences);
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
'/database/tldraw_fs/set_tldraw_node_file',
|
||||
'/database/tldraw_supabase/set_tldraw_node_file',
|
||||
snapshot,
|
||||
{
|
||||
params: {
|
||||
@@ -177,34 +318,37 @@ export class NavigationSnapshotService {
|
||||
const dbName = user.user_db_name;
|
||||
|
||||
logger.debug('snapshot-service', '📥 Loading snapshot', {
|
||||
nodePath: node.tldraw_snapshot,
|
||||
nodePath: node.node_storage_path,
|
||||
dbName,
|
||||
userType: user.user_type,
|
||||
username: user.username
|
||||
});
|
||||
|
||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||
node.tldraw_snapshot,
|
||||
node.node_storage_path,
|
||||
dbName,
|
||||
this.store,
|
||||
(state: LoadingState) => {
|
||||
if (state.status === 'ready') {
|
||||
this.currentNodePath = node.tldraw_snapshot;
|
||||
this.currentNodePath = node.node_storage_path;
|
||||
logger.debug('snapshot-service', '✅ Snapshot loaded and path updated', {
|
||||
nodePath: node.tldraw_snapshot
|
||||
nodePath: node.node_storage_path,
|
||||
currentNodePath: this.currentNodePath
|
||||
});
|
||||
} else if (state.status === 'error') {
|
||||
logger.error('snapshot-service', '❌ Error in load callback', {
|
||||
error: state.error,
|
||||
nodePath: node.tldraw_snapshot
|
||||
nodePath: node.node_storage_path
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined, // sharedStore
|
||||
this.editor || undefined // editor - use stored editor or fallback to store.loadSnapshot
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('snapshot-service', '❌ Failed to load navigation snapshot', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
nodePath: node.tldraw_snapshot
|
||||
nodePath: node.node_storage_path
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -240,16 +384,16 @@ export class NavigationSnapshotService {
|
||||
private async executeNavigation(fromNode: NavigationNode, toNode: NavigationNode): Promise<void> {
|
||||
try {
|
||||
logger.debug('snapshot-service', '🔄 Starting navigation snapshot handling', {
|
||||
from: fromNode.tldraw_snapshot,
|
||||
to: toNode.tldraw_snapshot,
|
||||
from: fromNode.node_storage_path,
|
||||
to: toNode.node_storage_path,
|
||||
currentPath: this.currentNodePath
|
||||
});
|
||||
|
||||
// If we're already in a navigation operation, queue this one
|
||||
if (this.isSaving || this.isLoading) {
|
||||
this.pendingOperation = {
|
||||
save: fromNode.tldraw_snapshot || undefined,
|
||||
load: toNode.tldraw_snapshot
|
||||
save: fromNode.node_storage_path || undefined,
|
||||
load: toNode.node_storage_path
|
||||
};
|
||||
logger.debug('snapshot-service', '⏳ Queued navigation operation', this.pendingOperation);
|
||||
return;
|
||||
@@ -261,10 +405,10 @@ export class NavigationSnapshotService {
|
||||
logger.debug('snapshot-service', '🧹 Cleared current node path');
|
||||
|
||||
// Load the new node's snapshot
|
||||
if (toNode.tldraw_snapshot) {
|
||||
if (toNode.node_storage_path) {
|
||||
await this.loadSnapshotForNode(toNode);
|
||||
logger.debug('snapshot-service', '✅ Loaded new node snapshot', {
|
||||
nodePath: toNode.tldraw_snapshot
|
||||
nodePath: toNode.node_storage_path
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,16 +418,16 @@ export class NavigationSnapshotService {
|
||||
const operation = this.pendingOperation;
|
||||
this.pendingOperation = null;
|
||||
await this.handleNavigationStart(
|
||||
operation.save ? { ...EMPTY_NODE, tldraw_snapshot: operation.save } : null,
|
||||
operation.load ? { ...EMPTY_NODE, tldraw_snapshot: operation.load } : null
|
||||
operation.save ? { ...EMPTY_NODE, node_storage_path: operation.save } : null,
|
||||
operation.load ? { ...EMPTY_NODE, node_storage_path: operation.load } : null
|
||||
);
|
||||
logger.debug('snapshot-service', '✅ Completed pending operation');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('snapshot-service', '❌ Error during navigation snapshot handling', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
fromPath: fromNode.tldraw_snapshot,
|
||||
toPath: toNode.tldraw_snapshot
|
||||
fromPath: fromNode.node_storage_path,
|
||||
toPath: toNode.node_storage_path
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -303,6 +447,8 @@ export class NavigationSnapshotService {
|
||||
async forceSaveCurrentNode(): Promise<void> {
|
||||
if (this.currentNodePath) {
|
||||
await this.saveCurrentSnapshot(this.currentNodePath);
|
||||
} else {
|
||||
logger.warn('snapshot-service', '⚠️ Cannot save - no current node path set');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user