Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
75f23c1
Add Memgraph graph database to Docker infrastructure
isiddharthsingh Jan 30, 2026
ecd3d3e
Add neo4j driver and services directory to server build
isiddharthsingh Jan 30, 2026
c618159
Add Memgraph client with Cypher CRUD and graph traversal
isiddharthsingh Jan 30, 2026
9067d7a
Add resource type mapper and graph batch writer
isiddharthsingh Jan 30, 2026
a97fe55
Add asset discovery providers for GCP, AWS, Azure, OVH, Scaleway, Tai…
isiddharthsingh Jan 30, 2026
74e2b7c
Add detail enrichment for Kubernetes, AWS, Azure, and serverless
isiddharthsingh Jan 30, 2026
885ea3d
Add 11-method connection inference engine for dependency detection
isiddharthsingh Jan 30, 2026
db48bd6
Add discovery orchestrator, Celery tasks, and graph API routes
isiddharthsingh Jan 30, 2026
ed2c283
Add graph discovery status indicators on connector cards
isiddharthsingh Jan 30, 2026
1d4d298
Fix discovery providers to use authenticated subprocess environments
isiddharthsingh Jan 30, 2026
8643acb
Add multi-project GCP discovery with post-auth wait and backend chaining
isiddharthsingh Jan 30, 2026
d2b9d16
Fix GCP VPC extraction and network proximity inference for relationsh…
isiddharthsingh Jan 30, 2026
e84326f
Fix frontend discovery trigger timing for GCP post-auth flow
isiddharthsingh Jan 30, 2026
683e803
Fix Azure discovery: CLI login, NSG inference, DB pool, and resource …
isiddharthsingh Feb 1, 2026
59dbf97
Add Azure discovery trigger and fix stale task polling
isiddharthsingh Feb 1, 2026
764f422
Fix OVH discovery: correct CLI name, flags, commands, and auto-discov…
isiddharthsingh Feb 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ WEAVIATE_HOST=weaviate
WEAVIATE_PORT=8080
WEAVIATE_GRPC_PORT=50051

# -----------------------------------------------------------------------------
# Memgraph (Graph Database)
# -----------------------------------------------------------------------------
MEMGRAPH_HOST=memgraph
MEMGRAPH_PORT=7687
MEMGRAPH_USER=aurora
MEMGRAPH_PASSWORD=aurora_secure_password
DISCOVERY_INTERVAL_HOURS=1

# -----------------------------------------------------------------------------
# Rate Limiting
# -----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions client/src/app/aws/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export default function AWSOnboardingPage() {

await fetchOnboardingData();
setIsConfigured(true);
localStorage.setItem("aurora_graph_discovery_trigger", "1");

} catch (err) {
console.error("Failed to set role:", err);
Expand Down
1 change: 1 addition & 0 deletions client/src/app/azure/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ export default function AzureAuthPage() {
localStorage.setItem("cloudProvider", "azure");
localStorage.setItem("isAzureFetched", "true"); // Data already fetched during connection
localStorage.setItem("isLoggedAurora", "true");
localStorage.setItem("aurora_graph_discovery_trigger", "1");

// Auto-select Azure provider when connection succeeds (legitimate connection)
const { providerPreferencesService } = await import('@/lib/services/providerPreferences');
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/ovh/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export default function OvhOnboardingPage() {
const data = await response.json();

if (data.authorizationUrl) {
// Signal graph discovery to trigger after OAuth completes
localStorage.setItem("aurora_graph_discovery_trigger", "1");
// Redirect to OVH authorization page
window.location.href = data.authorizationUrl;
} else {
Expand Down
1 change: 1 addition & 0 deletions client/src/app/scaleway/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default function ScalewayOnboardingPage() {

// Success - update localStorage and redirect
localStorage.setItem("isScalewayConnected", "true");
localStorage.setItem("aurora_graph_discovery_trigger", "1");

// Auto-select Scaleway provider when connection succeeds (legitimate connection)
await providerPreferencesService.smartAutoSelect('scaleway', true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,9 @@ export class ProviderPolling {
localStorage.setItem("isGCPFetched", "false");
localStorage.setItem("isLoggedAurora", "true");

// Trigger graph discovery now that post-auth (API enablement, SA propagation) is done
localStorage.setItem("aurora_graph_discovery_trigger", "1");

// Update provider state first and clear setup status
this.config.onProvidersUpdate(prev => prev.map(p =>
p.id === 'gcp' ? {
Expand Down
30 changes: 28 additions & 2 deletions client/src/components/connectors/ConnectorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ConnectorDialogs } from "./ConnectorDialogs";
import { ConnectorCardContent } from "./ConnectorCardContent";
import type { ConnectorConfig } from "./types";
import { useGitHubStatus } from "@/hooks/use-github-status";
import { useGraphDiscoveryStatus } from "@/hooks/use-graph-discovery-status";

const slackService = isSlackEnabled() ? require("@/lib/services/slack").slackService : null;

Expand Down Expand Up @@ -43,6 +44,9 @@ export default function ConnectorCard({ connector }: ConnectorCardProps) {
checkGitHubStatus,
} = useConnectorStatus(connector, userId);

// Graph discovery status (only active for supported cloud providers)
const { syncStatus } = useGraphDiscoveryStatus(connector.id, isConnected, userId);

const {
isConnecting: isConnectingOAuthHandler,
handleGitHubOAuth,
Expand Down Expand Up @@ -223,18 +227,40 @@ export default function ConnectorCard({ connector }: ConnectorCardProps) {

<CardContent className="flex-1">
{connector.id === "slack" && isConnected ? (
<ConnectorCardContent
<ConnectorCardContent
isLoading={isLoadingDetails}
slackStatus={slackStatus}
description={connector.description}
/>
) : (
<ConnectorCardContent
<ConnectorCardContent
isLoading={false}
slackStatus={null}
description={connector.description}
/>
)}
{syncStatus !== "idle" && (
<div className="flex items-center gap-1.5 mt-2 text-xs">
{syncStatus === "building" && (
<>
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
<span className="text-muted-foreground">Building dependency graph...</span>
</>
)}
{syncStatus === "synced" && (
<>
<Check className="h-3 w-3 text-green-600 dark:text-green-500" />
<span className="text-green-600 dark:text-green-500">Graph synced</span>
</>
)}
{syncStatus === "error" && (
<>
<AlertCircle className="h-3 w-3 text-red-600 dark:text-red-500" />
<span className="text-red-600 dark:text-red-500">Graph sync failed</span>
</>
)}
</div>
)}
</CardContent>

<CardFooter className="flex flex-col gap-2">
Expand Down
4 changes: 4 additions & 0 deletions client/src/hooks/use-connector-oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export function useConnectorOAuth(connector: ConnectorConfig, userId: string | n
const data = await response.json();

if (data.login_url) {
// Signal the frontend discovery hook to start showing status.
// For GCP, the actual discovery task is chained from the backend
// after post-auth completes (gcp_post_auth_tasks.py).
localStorage.setItem("aurora_graph_discovery_trigger", "1");
window.location.href = data.login_url;
} else {
throw new Error("No OAuth URL received");
Expand Down
182 changes: 182 additions & 0 deletions client/src/hooks/use-graph-discovery-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { useEffect, useRef, useState, useCallback } from "react";
import {
GRAPH_DISCOVERY_PROVIDERS,
triggerGraphDiscovery,
pollDiscoveryStatus,
} from "@/lib/services/graph-discovery";

export type GraphSyncStatus = "idle" | "building" | "synced" | "error";

const STORAGE_KEY = "aurora_graph_discovery_task";
const TRIGGER_KEY = "aurora_graph_discovery_trigger";
const POLL_INTERVAL = 5_000;
const SYNCED_DISPLAY_MS = 4_000;
const MAX_TASK_AGE_MS = 30 * 60 * 1000; // 30 minutes
const MAX_POLL_ERRORS = 6; // Give up after 6 consecutive failures (30s)

interface StoredTask {
taskId: string;
userId: string;
startedAt: number;
}

function getStoredTask(): StoredTask | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const task: StoredTask = JSON.parse(raw);
// Expire stale tasks (e.g. worker restarted and lost the task)
if (Date.now() - task.startedAt > MAX_TASK_AGE_MS) {
localStorage.removeItem(STORAGE_KEY);
return null;
}
return task;
} catch {
return null;
}
}

function setStoredTask(task: StoredTask) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(task));
}

function clearStoredTask() {
localStorage.removeItem(STORAGE_KEY);
}

/**
* Tracks graph discovery status for a connector card.
*
* Trigger: listens for the `providerStateChanged` custom event, which is
* dispatched explicitly by connection flows (GCP OAuth callback, AWS
* onboarding, OVH/Scaleway integration, etc.) — NOT by periodic status
* checks. The backend Redis dedup prevents duplicate concurrent tasks.
*
* All provider cards share a single task via localStorage.
*/
export function useGraphDiscoveryStatus(
connectorId: string,
isConnected: boolean,
userId: string | null
): { syncStatus: GraphSyncStatus } {
const [syncStatus, setSyncStatus] = useState<GraphSyncStatus>("idle");
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const syncedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const supportsDiscovery = (GRAPH_DISCOVERY_PROVIDERS as readonly string[]).includes(connectorId);

const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
}, []);

const startPolling = useCallback(
(taskId: string, uid: string) => {
setSyncStatus("building");
stopPolling();

let errorCount = 0;
const poll = async () => {
try {
const status = await pollDiscoveryStatus(uid, taskId);
errorCount = 0; // Reset on success
if (!status.complete) return;

stopPolling();
clearStoredTask();

if (status.error) {
setSyncStatus("error");
} else {
setSyncStatus("synced");
syncedTimerRef.current = setTimeout(() => {
setSyncStatus("idle");
}, SYNCED_DISPLAY_MS);
}
} catch {
errorCount++;
if (errorCount >= MAX_POLL_ERRORS) {
stopPolling();
clearStoredTask();
setSyncStatus("idle");
}
}
};

poll();
pollTimerRef.current = setInterval(poll, POLL_INTERVAL);
},
[stopPolling]
);

const triggerDiscovery = useCallback(
async (uid: string) => {
// If already polling, skip
if (pollTimerRef.current) return;

// If a task is stored, resume polling instead of triggering a new one
const stored = getStoredTask();
if (stored && stored.userId === uid) {
startPolling(stored.taskId, uid);
return;
}

try {
const resp = await triggerGraphDiscovery(uid);
// Backend returns status "already_running" if a task is active
setStoredTask({ taskId: resp.task_id, userId: uid, startedAt: Date.now() });
startPolling(resp.task_id, uid);
} catch {
setSyncStatus("error");
}
},
[startPolling]
);

// Check trigger flag helper (used on mount and on providerStateChanged)
const checkTrigger = useCallback(
(uid: string) => {
// Resume polling if a task is already in-flight
const stored = getStoredTask();
if (stored && stored.userId === uid) {
startPolling(stored.taskId, uid);
return;
}

// Check if a connection flow just completed (set by post-auth completion,
// onboarding pages, etc.)
const trigger = localStorage.getItem(TRIGGER_KEY);
if (trigger) {
localStorage.removeItem(TRIGGER_KEY);
triggerDiscovery(uid);
}
},
[startPolling, triggerDiscovery]
);

useEffect(() => {
if (!supportsDiscovery || !isConnected || !userId) return;

checkTrigger(userId);

// Also listen for providerStateChanged — GCP post-auth sets the trigger
// flag and dispatches this event AFTER isConnected is already true, so
// the deps-based re-run won't catch it.
const uid = userId;
const onProviderChange = () => checkTrigger(uid);
window.addEventListener("providerStateChanged", onProviderChange);

return () => {
stopPolling();
if (syncedTimerRef.current) clearTimeout(syncedTimerRef.current);
window.removeEventListener("providerStateChanged", onProviderChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [supportsDiscovery, isConnected, userId]);

if (!supportsDiscovery) return { syncStatus: "idle" };

return { syncStatus };
}
49 changes: 49 additions & 0 deletions client/src/lib/services/graph-discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL;

/** Provider IDs that support graph discovery. */
export const GRAPH_DISCOVERY_PROVIDERS = [
"gcp",
"aws",
"azure",
"ovh",
"scaleway",
"tailscale",
] as const;

/** Trigger a full graph discovery run for the user. */
export async function triggerGraphDiscovery(
userId: string
): Promise<{ task_id: string }> {
const res = await fetch(`${BACKEND_URL}/api/graph/discover`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-User-ID": userId,
},
});
if (!res.ok) throw new Error("Failed to trigger graph discovery");
return res.json();
}

export interface DiscoveryStatus {
state: string;
status: string;
complete: boolean;
error?: boolean;
result?: Record<string, unknown>;
}

/** Poll the status of an in-flight discovery task. */
export async function pollDiscoveryStatus(
userId: string,
taskId: string
): Promise<DiscoveryStatus> {
const res = await fetch(
`${BACKEND_URL}/api/graph/discover/status/${taskId}`,
{
headers: { "X-User-ID": userId },
}
);
if (!res.ok) throw new Error("Failed to poll discovery status");
return res.json();
}
Loading
Loading