// visitor-recent-by-person — list recent :Session activity for a :Person. // Walks (Person)-[:VISITED]->(Session)-[:HAS_EVENT]->(*) and returns a // chronological summary the morning-round skill folds into its briefing. import { int } from "neo4j-driver"; import { getSession } from "../lib/neo4j.js"; export interface RecentByPersonParams { accountId: string; personId: string; sinceDays?: number; limit?: number; } export interface SessionSummary { sessionId: string; startedAt: string; lastSeenAt: string; pageviewCount: number; clickCount: number; maxScrollDepth: number; listingSlugs: string[]; dwellMs: number; } export async function visitorRecentByPerson(p: RecentByPersonParams): Promise<{ sessions: SessionSummary[] }> { const since = p.sinceDays ?? 30; const limit = Math.max(1, Math.min(p.limit ?? 50, 200)); const session = getSession(); try { const result = await session.run( `MATCH (per:Person {accountId: $accountId}) WHERE elementId(per) = $personId MATCH (per)-[:VISITED]->(s:Session) WHERE s.startedAt >= datetime() - duration({days: $since}) OPTIONAL MATCH (s)-[:HAS_EVENT]->(pv:PageView) OPTIONAL MATCH (s)-[:HAS_EVENT]->(cl:Click) OPTIONAL MATCH (s)-[:HAS_EVENT]->(sm:ScrollMilestone) OPTIONAL MATCH (pv)-[:OF_LISTING]->(l:Listing) WITH s, count(DISTINCT pv) AS pageviews, count(DISTINCT cl) AS clicks, coalesce(max(sm.maxDepth), 0) AS maxDepth, collect(DISTINCT l.slug) AS slugs, coalesce(sum(pv.dwellMs), 0) AS dwellMs RETURN s.sessionId AS sessionId, toString(s.startedAt) AS startedAt, toString(s.lastSeenAt) AS lastSeenAt, pageviews, clicks, maxDepth, slugs, dwellMs ORDER BY s.lastSeenAt DESC LIMIT $limit`, { accountId: p.accountId, personId: p.personId, since, limit: int(limit) }, ); return { sessions: result.records.map((r) => ({ sessionId: r.get("sessionId"), startedAt: r.get("startedAt"), lastSeenAt: r.get("lastSeenAt"), pageviewCount: Number(r.get("pageviews") ?? 0), clickCount: Number(r.get("clicks") ?? 0), maxScrollDepth: Number(r.get("maxDepth") ?? 0), listingSlugs: (r.get("slugs") ?? []).filter((s: unknown): s is string => typeof s === "string"), dwellMs: Number(r.get("dwellMs") ?? 0), })), }; } finally { await session.close(); } }