package main import ( "context" "crypto/rand" "fmt" "log" "os" "path/filepath" "strings" "sync" "time" ops "fn-registry/fn_operations" "fn-registry/registry" ) // App is the Wails-bound application struct. type App struct { ctx context.Context mu sync.RWMutex projectsDir string registryRoot string registryDB *registry.DB db *ops.DB currentProj string } // NewApp creates a new App instance. func NewApp(projectsDir, registryRoot string) *App { log.Printf("[NewApp] projectsDir=%s registryRoot=%s", projectsDir, registryRoot) return &App{ projectsDir: projectsDir, registryRoot: registryRoot, } } func (a *App) startup(ctx context.Context) { a.ctx = ctx log.Println("[startup] called") // Open registry.db for type lookups and snapshots regPath := filepath.Join(a.registryRoot, "registry.db") log.Printf("[startup] opening registry.db at %s", regPath) if db, err := registry.Open(regPath); err == nil { a.registryDB = db log.Println("[startup] registry.db opened OK") } else { log.Printf("[startup] WARNING: registry.db failed: %v", err) } // Ensure projects directory exists log.Printf("[startup] ensuring projectsDir exists: %s", a.projectsDir) if err := os.MkdirAll(a.projectsDir, 0o755); err != nil { log.Printf("[startup] ERROR creating projectsDir: %v", err) } // List what's there already entries, err := os.ReadDir(a.projectsDir) if err != nil { log.Printf("[startup] ERROR reading projectsDir: %v", err) } else { log.Printf("[startup] projectsDir has %d entries", len(entries)) for _, e := range entries { log.Printf("[startup] - %s (dir=%v)", e.Name(), e.IsDir()) } } log.Println("[startup] done") } func (a *App) shutdown(_ context.Context) { log.Println("[shutdown] called") a.mu.Lock() defer a.mu.Unlock() if a.db != nil { a.db.Close() log.Println("[shutdown] closed project db") } if a.registryDB != nil { a.registryDB.Close() log.Println("[shutdown] closed registry db") } } // --- Projects --- func (a *App) ListProjects() ([]ProjectInfo, error) { a.mu.RLock() defer a.mu.RUnlock() log.Printf("[ListProjects] projectsDir=%s", a.projectsDir) projects, err := listProjectDirs(a.projectsDir) if err != nil { log.Printf("[ListProjects] ERROR: %v", err) return nil, err } log.Printf("[ListProjects] found %d projects", len(projects)) for _, p := range projects { log.Printf("[ListProjects] - %s (entities=%d relations=%d)", p.Name, p.EntityCount, p.RelCount) } return projects, nil } func (a *App) CreateProject(name string) (ProjectInfo, error) { a.mu.Lock() defer a.mu.Unlock() name = strings.TrimSpace(name) log.Printf("[CreateProject] name=%q projectsDir=%s", name, a.projectsDir) if name == "" { log.Println("[CreateProject] ERROR: empty name") return ProjectInfo{}, fmt.Errorf("project name cannot be empty") } targetDir := filepath.Join(a.projectsDir, name) log.Printf("[CreateProject] targetDir=%s", targetDir) if err := createProject(a.projectsDir, name); err != nil { log.Printf("[CreateProject] ERROR: %v", err) return ProjectInfo{}, err } // Verify it was created dbPath := filepath.Join(targetDir, "operations.db") if _, err := os.Stat(dbPath); err != nil { log.Printf("[CreateProject] WARNING: operations.db not found after create: %v", err) } else { log.Printf("[CreateProject] OK: operations.db exists at %s", dbPath) } log.Printf("[CreateProject] success: %s", name) return ProjectInfo{Name: name}, nil } func (a *App) SwitchProject(name string) error { a.mu.Lock() defer a.mu.Unlock() log.Printf("[SwitchProject] name=%q (current=%q)", name, a.currentProj) if a.db != nil { a.db.Close() a.db = nil log.Println("[SwitchProject] closed previous db") } dbPath := filepath.Join(a.projectsDir, name, "operations.db") log.Printf("[SwitchProject] opening %s", dbPath) db, err := ops.Open(dbPath) if err != nil { log.Printf("[SwitchProject] ERROR: %v", err) return fmt.Errorf("opening project %s: %w", name, err) } a.db = db a.currentProj = name log.Printf("[SwitchProject] OK: now on project %s", name) return nil } func (a *App) DeleteProject(name string) error { a.mu.Lock() defer a.mu.Unlock() log.Printf("[DeleteProject] name=%q", name) if a.currentProj == name && a.db != nil { a.db.Close() a.db = nil a.currentProj = "" log.Println("[DeleteProject] closed active project db") } err := deleteProject(a.projectsDir, name) if err != nil { log.Printf("[DeleteProject] ERROR: %v", err) } else { log.Printf("[DeleteProject] OK: deleted %s", name) } return err } func (a *App) GetCurrentProject() string { a.mu.RLock() defer a.mu.RUnlock() log.Printf("[GetCurrentProject] -> %q", a.currentProj) return a.currentProj } // --- Entities --- func (a *App) ListEntities() ([]ops.Entity, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { log.Println("[ListEntities] ERROR: no project selected") return nil, fmt.Errorf("no project selected") } entities, err := a.db.ListEntities("", "") if err != nil { log.Printf("[ListEntities] ERROR: %v", err) } else { log.Printf("[ListEntities] found %d entities", len(entities)) } return entities, err } func (a *App) GetEntity(id string) (*ops.Entity, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { return nil, fmt.Errorf("no project selected") } log.Printf("[GetEntity] id=%s", id) return a.db.GetEntity(id) } func (a *App) AddEntity(input EntityInput) (string, error) { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { log.Println("[AddEntity] ERROR: no project selected") return "", fmt.Errorf("no project selected") } id := makeEntityID(input.Name, input.TypeRef) log.Printf("[AddEntity] name=%q typeRef=%q -> id=%s", input.Name, input.TypeRef, id) now := time.Now() e := &ops.Entity{ ID: id, Name: input.Name, TypeRef: input.TypeRef, Status: ops.StatusActive, Description: input.Description, Domain: "osint", Tags: input.Tags, Source: "fuzzygraph", Metadata: input.Metadata, Notes: input.Notes, CreatedAt: now, UpdatedAt: now, } // Use InsertEntityWithSnapshot if registry is available if a.registryDB != nil { log.Println("[AddEntity] using InsertEntityWithSnapshot") if err := ops.InsertEntityWithSnapshot(a.db, a.registryDB, e); err != nil { log.Printf("[AddEntity] ERROR: %v", err) return "", err } } else { log.Println("[AddEntity] using plain InsertEntity (no registry)") if err := a.db.InsertEntity(e); err != nil { log.Printf("[AddEntity] ERROR: %v", err) return "", err } } log.Printf("[AddEntity] OK: %s", id) return id, nil } func (a *App) UpdateEntity(id string, input EntityInput) error { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return fmt.Errorf("no project selected") } log.Printf("[UpdateEntity] id=%s", id) existing, err := a.db.GetEntity(id) if err != nil { log.Printf("[UpdateEntity] ERROR getting: %v", err) return err } if existing == nil { log.Printf("[UpdateEntity] ERROR: not found") return fmt.Errorf("entity %s not found", id) } existing.Name = input.Name existing.TypeRef = input.TypeRef existing.Description = input.Description existing.Tags = input.Tags existing.Metadata = input.Metadata existing.Notes = input.Notes existing.UpdatedAt = time.Now() if err := a.db.UpdateEntity(existing); err != nil { log.Printf("[UpdateEntity] ERROR: %v", err) return err } log.Printf("[UpdateEntity] OK: %s", id) return nil } func (a *App) DeleteEntity(id string) error { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return fmt.Errorf("no project selected") } log.Printf("[DeleteEntity] id=%s", id) return a.db.DeleteEntity(id) } // --- Relations --- func (a *App) ListRelations() ([]ops.Relation, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { log.Println("[ListRelations] ERROR: no project selected") return nil, fmt.Errorf("no project selected") } relations, err := a.db.ListRelations("") if err != nil { log.Printf("[ListRelations] ERROR: %v", err) } else { log.Printf("[ListRelations] found %d relations", len(relations)) } return relations, err } func (a *App) AddRelation(input RelationInputDTO) (string, error) { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return "", fmt.Errorf("no project selected") } id := generateID() log.Printf("[AddRelation] name=%q from=%s to=%s id=%s", input.Name, input.FromEntity, input.ToEntity, id) now := time.Now() r := &ops.Relation{ ID: id, Name: input.Name, FromEntity: input.FromEntity, ToEntity: input.ToEntity, Description: input.Description, Purity: "impure", Direction: ops.DirUnidirectional, Weight: input.Weight, Status: ops.RelImplemented, Tags: input.Tags, Notes: input.Notes, CreatedAt: now, UpdatedAt: now, } if err := a.db.InsertRelation(r); err != nil { log.Printf("[AddRelation] ERROR: %v", err) return "", err } log.Printf("[AddRelation] OK: %s", id) return id, nil } func (a *App) UpdateRelation(id string, input RelationInputDTO) error { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return fmt.Errorf("no project selected") } log.Printf("[UpdateRelation] id=%s", id) existing, err := a.db.GetRelation(id) if err != nil { return err } if existing == nil { return fmt.Errorf("relation %s not found", id) } existing.Name = input.Name existing.FromEntity = input.FromEntity existing.ToEntity = input.ToEntity existing.Description = input.Description existing.Weight = input.Weight existing.Tags = input.Tags existing.Notes = input.Notes existing.UpdatedAt = time.Now() return a.db.UpdateRelation(existing) } func (a *App) DeleteRelation(id string) error { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return fmt.Errorf("no project selected") } log.Printf("[DeleteRelation] id=%s", id) return a.db.DeleteRelation(id) } // --- Graph --- func (a *App) GetGraphData() (GraphData, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { log.Println("[GetGraphData] no project selected") return GraphData{}, fmt.Errorf("no project selected") } data, err := buildGraphData(a.db) if err != nil { log.Printf("[GetGraphData] ERROR: %v", err) } else { log.Printf("[GetGraphData] nodes=%d edges=%d", len(data.Nodes), len(data.Edges)) } return data, err } func (a *App) GetEntityNeighbors(entityID string, depth int) (GraphData, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { return GraphData{}, fmt.Errorf("no project selected") } log.Printf("[GetEntityNeighbors] entityID=%s depth=%d", entityID, depth) return buildEgoGraph(a.db, entityID, depth) } func (a *App) GetFilteredGraph(typeFilters []string) (GraphData, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { return GraphData{}, fmt.Errorf("no project selected") } log.Printf("[GetFilteredGraph] filters=%v", typeFilters) full, err := buildGraphData(a.db) if err != nil { return GraphData{}, err } if len(typeFilters) == 0 { return full, nil } allowed := map[string]bool{} for _, t := range typeFilters { allowed[t] = true } nodeIDs := map[string]bool{} var nodes []GraphNode for _, n := range full.Nodes { if allowed[n.Type] { nodes = append(nodes, n) nodeIDs[n.ID] = true } } var edges []GraphEdge for _, e := range full.Edges { if nodeIDs[e.Source] && nodeIDs[e.Target] { edges = append(edges, e) } } return GraphData{Nodes: nodes, Edges: edges}, nil } // --- Search --- func (a *App) SearchEntities(query string) ([]ops.Entity, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { return nil, fmt.Errorf("no project selected") } log.Printf("[SearchEntities] query=%q", query) results, err := searchEntitiesFTS(a.db, query) if err != nil { log.Printf("[SearchEntities] ERROR: %v", err) } else { log.Printf("[SearchEntities] found %d results", len(results)) } return results, err } func (a *App) SearchGraph(query string) (GraphData, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { return GraphData{}, fmt.Errorf("no project selected") } log.Printf("[SearchGraph] query=%q", query) return searchGraph(a.db, query) } // --- Assertions --- func (a *App) ListAssertions(entityID string) ([]ops.Assertion, error) { a.mu.RLock() defer a.mu.RUnlock() if a.db == nil { return nil, fmt.Errorf("no project selected") } log.Printf("[ListAssertions] entityID=%q", entityID) active := true return a.db.ListAssertions(entityID, &active) } func (a *App) AddAssertion(input AssertionInput) (string, error) { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return "", fmt.Errorf("no project selected") } id := generateID() log.Printf("[AddAssertion] name=%q entityID=%s id=%s", input.Name, input.EntityID, id) assertion := &ops.Assertion{ ID: id, EntityID: input.EntityID, Name: input.Name, Kind: input.Kind, Rule: input.Rule, Severity: ops.Severity(input.Severity), Description: input.Description, Active: true, CreatedAt: time.Now(), } if err := a.db.InsertAssertion(assertion); err != nil { log.Printf("[AddAssertion] ERROR: %v", err) return "", err } log.Printf("[AddAssertion] OK: %s", id) return id, nil } func (a *App) DeleteAssertion(id string) error { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return fmt.Errorf("no project selected") } log.Printf("[DeleteAssertion] id=%s", id) return a.db.DeleteAssertion(id) } func (a *App) EvalAssertions(entityID string) ([]ops.AssertionResult, error) { a.mu.Lock() defer a.mu.Unlock() if a.db == nil { return nil, fmt.Errorf("no project selected") } log.Printf("[EvalAssertions] entityID=%s", entityID) results, err := ops.EvalEntityAssertions(a.db, entityID, "") if err != nil { log.Printf("[EvalAssertions] ERROR: %v", err) } else { log.Printf("[EvalAssertions] %d results", len(results)) } return results, err } // --- Presets --- func (a *App) GetEntityPresets() []EntityTypePreset { log.Printf("[GetEntityPresets] returning %d presets", len(entityTypePresets)) return entityTypePresets } func (a *App) GetRelationPresets() []string { log.Printf("[GetRelationPresets] returning %d presets", len(relationPresets)) return relationPresets } // --- Helpers --- func makeEntityID(name, typeRef string) string { clean := strings.ToLower(strings.TrimSpace(name)) clean = strings.ReplaceAll(clean, " ", "_") clean = strings.ReplaceAll(clean, "-", "_") parts := strings.Split(typeRef, "_") shortType := typeRef if len(parts) >= 2 { shortType = parts[1] } return fmt.Sprintf("%s_%s", clean, shortType) } func generateID() string { b := make([]byte, 16) rand.Read(b) b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) }