puter fix: service registry and curator onboaridng data fix, home feed route fixes
This commit is contained in:
@@ -15,10 +15,10 @@ const requiredServices = [
|
||||
"courses-service",
|
||||
"assessment-service",
|
||||
"matchmaking-service",
|
||||
"pathways-service",
|
||||
"resume-service",
|
||||
"cover-letter-service",
|
||||
"qscore-service",
|
||||
"social-branding-service",
|
||||
];
|
||||
|
||||
const registry = await import("../dist/services/service-registry.js");
|
||||
@@ -95,7 +95,6 @@ for (const id of requiredServices) {
|
||||
}
|
||||
|
||||
assert(registry.getService("jobs-service")?.id === "matchmaking-service", "matchmaking alias failed");
|
||||
assert(registry.getService("career-pathways-service")?.id === "pathways-service", "pathways alias failed");
|
||||
assert(registry.getService("coverletter-service")?.id === "cover-letter-service", "cover-letter alias failed");
|
||||
assert(registry.getService("q-score-service")?.id === "qscore-service", "qscore alias failed");
|
||||
assert(registry.getService("social-service")?.id === "social-branding-service", "social alias failed");
|
||||
|
||||
@@ -27,7 +27,6 @@ const requiredServices = [
|
||||
"courses-service",
|
||||
"assessment-service",
|
||||
"matchmaking-service",
|
||||
"pathways-service",
|
||||
"qscore-service",
|
||||
"social-branding-service",
|
||||
];
|
||||
|
||||
@@ -52,8 +52,8 @@ Every notification must point to one of these real dashboard routes:
|
||||
- /agents/roleplay for recruiter/manager/salary/stakeholder roleplay
|
||||
- /agents/qscore for Q Score/readiness explanations
|
||||
- /missions for mission progress, approvals, artifacts, next stages
|
||||
- /social for LinkedIn/social branding
|
||||
- /pathways for locked/coming-soon pathways
|
||||
- /agents/social-branding for LinkedIn/social branding
|
||||
- /agents/matchmaking for Scout/opportunity matching
|
||||
- /rewards for locked/coming-soon rewards
|
||||
- /suggestions for broad onboarding/profile suggestions
|
||||
Every notification object must include:
|
||||
@@ -65,11 +65,10 @@ Use urgency truthfully: now = needs immediate user action, today = useful today,
|
||||
function sanitizeHref(href: string, moduleId: HomeModuleId) {
|
||||
if (isAllowedNotificationHref(href)) return href;
|
||||
if (href.startsWith("/missions")) return "/missions/active";
|
||||
if (href.startsWith("/social")) return "/social";
|
||||
if (href.startsWith("/pathways")) return "/pathways";
|
||||
if (href.startsWith("/agents/social-branding")) return "/agents/social-branding";
|
||||
if (href.startsWith("/agents/matchmaking")) return "/agents/matchmaking";
|
||||
if (href.startsWith("/rewards")) return "/rewards";
|
||||
if (href.startsWith("/productivity")) return "/productivity";
|
||||
return moduleId === "productivity" ? "/productivity" : `/${moduleId}`;
|
||||
return moduleId === "productivity" ? "/agents" : `/${moduleId}`;
|
||||
}
|
||||
|
||||
function stableId(prefix: string, index: number) {
|
||||
@@ -82,8 +81,8 @@ function sourceFromHref(href: string) {
|
||||
if (href.startsWith("/agents/roleplay")) return "roleplay";
|
||||
if (href.startsWith("/agents/qscore")) return "qscore";
|
||||
if (href.startsWith("/missions")) return "mission";
|
||||
if (href.startsWith("/social")) return "social";
|
||||
if (href.startsWith("/pathways")) return "pathways";
|
||||
if (href.startsWith("/agents/social-branding")) return "social";
|
||||
if (href.startsWith("/agents/matchmaking")) return "pathways";
|
||||
if (href.startsWith("/rewards")) return "rewards";
|
||||
return "system";
|
||||
}
|
||||
@@ -115,8 +114,8 @@ function defaultHrefForSource(source: NonNullable<AgentHomeNotification["source"
|
||||
if (source === "roleplay") return "/agents/roleplay";
|
||||
if (source === "qscore") return "/agents/qscore";
|
||||
if (source === "mission") return "/missions";
|
||||
if (source === "social") return "/social";
|
||||
if (source === "pathways") return "/pathways";
|
||||
if (source === "social") return "/agents/social-branding";
|
||||
if (source === "pathways") return "/agents/matchmaking";
|
||||
if (source === "rewards") return "/rewards";
|
||||
return moduleId === "productivity" ? "/agents" : `/${moduleId}`;
|
||||
}
|
||||
|
||||
@@ -37,16 +37,16 @@ const FRESH_MS = 10 * 60 * 1000;
|
||||
const EXPIRY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const SERVICE_HREFS = {
|
||||
resume: buildServiceLink("resume-service", "workspace") ?? "/opportunities/resume",
|
||||
interview: buildServiceLink("interview-service", "discovery") ?? "/upskilling/interview",
|
||||
roleplay: buildServiceLink("roleplay-service", "discovery") ?? "/upskilling/roleplay",
|
||||
qscore: buildServiceLink("qscore-service", "dashboard") ?? "/home",
|
||||
resume: buildServiceLink("resume-service", "workspace") ?? "/agents/resume",
|
||||
interview: buildServiceLink("interview-service", "discovery") ?? "/agents/interview",
|
||||
roleplay: buildServiceLink("roleplay-service", "discovery") ?? "/agents/roleplay",
|
||||
qscore: buildServiceLink("qscore-service", "dashboard") ?? "/agents/qscore",
|
||||
mission: "/missions/active",
|
||||
social: buildServiceLink("social-branding-service", "profile") ?? "/opportunities/social-media",
|
||||
pathways: buildServiceLink("pathways-service", "dashboard") ?? "/career-pathways",
|
||||
social: buildServiceLink("social-branding-service", "profile") ?? "/agents/social-branding",
|
||||
pathways: buildServiceLink("matchmaking-service", "jobs") ?? "/agents/matchmaking",
|
||||
rewards: "/rewards",
|
||||
suggestions: "/suggestions",
|
||||
productivity: "/productivity",
|
||||
productivity: buildServiceLink("courses-service", "catalog") ?? "/agents/courses",
|
||||
} as const;
|
||||
|
||||
type SeedNotification = Omit<HomeNotification, "id" | "createdAt"> & { moduleId: HomeModuleId; priority: number };
|
||||
|
||||
@@ -25,12 +25,12 @@ const demoNotifications: DemoNotification[] = [
|
||||
{ moduleId: "missions", title: "2 approvals pending", subtitle: "Resume v6 and Mock #4 feedback need your confirmation.", tag: "Action", urgency: "now", href: "/missions", source: "mission", priority: 113 },
|
||||
{ moduleId: "missions", title: "Final readiness unlock", subtitle: "Complete one roleplay recovery drill to generate the final checklist.", tag: "Next", urgency: "soon", href: "/missions", source: "mission", priority: 106 },
|
||||
|
||||
{ moduleId: "social", title: "LinkedIn headline v3 ready", subtitle: "Clearer target: Product Intern · FinTech · Growth Systems.", tag: "Ready", urgency: "today", href: "/social", source: "social", priority: 104 },
|
||||
{ moduleId: "social", title: "Featured section has 3 proof pins", subtitle: "Use resume scan, mock review, and Q Score delta as credibility blocks.", tag: "Proof", urgency: "soon", href: "/social", source: "social", priority: 100 },
|
||||
{ moduleId: "social", title: "Banner options queued", subtitle: "Three calm orange/blue layouts are waiting for review.", tag: "Brand", urgency: "soon", href: "/social", source: "social", priority: 96 },
|
||||
{ moduleId: "social", title: "LinkedIn headline v3 ready", subtitle: "Clearer target: Product Intern · FinTech · Growth Systems.", tag: "Ready", urgency: "today", href: "/agents/social-branding", source: "social", priority: 104 },
|
||||
{ moduleId: "social", title: "Featured section has 3 proof pins", subtitle: "Use resume scan, mock review, and Q Score delta as credibility blocks.", tag: "Proof", urgency: "soon", href: "/agents/social-branding", source: "social", priority: 100 },
|
||||
{ moduleId: "social", title: "Banner options queued", subtitle: "Three calm orange/blue layouts are waiting for review.", tag: "Brand", urgency: "soon", href: "/agents/social-branding", source: "social", priority: 96 },
|
||||
|
||||
{ moduleId: "pathways", title: "Pathways stay locked for demo", subtitle: "Resume + interview data is enough; pathway service is not enabled yet.", tag: "Soon", urgency: "calm", href: "/pathways", source: "pathways", priority: 70 },
|
||||
{ moduleId: "pathways", title: "PM SaaS route predicted", subtitle: "Directional only until the pathways service is connected.", tag: "Preview", urgency: "calm", href: "/pathways", source: "pathways", priority: 68 },
|
||||
{ moduleId: "pathways", title: "Pathways stay locked for demo", subtitle: "Resume + interview data is enough; pathway service is not enabled yet.", tag: "Soon", urgency: "calm", href: "/agents/matchmaking", source: "pathways", priority: 70 },
|
||||
{ moduleId: "pathways", title: "PM SaaS route predicted", subtitle: "Directional only until the pathways service is connected.", tag: "Preview", urgency: "calm", href: "/agents/matchmaking", source: "pathways", priority: 68 },
|
||||
|
||||
{ moduleId: "productivity", title: "Resume ATS 86", subtitle: "Keyword relevance +9 after JD tailoring. Open Resume Builder.", tag: "Resume", urgency: "today", href: "/agents/resume", source: "resume", priority: 118 },
|
||||
{ moduleId: "productivity", title: "Interview review is ready", subtitle: "Overall 82 · storytelling and concise examples need one more drill.", tag: "Mock", urgency: "now", href: "/agents/interview", source: "interview", priority: 117 },
|
||||
|
||||
@@ -42,8 +42,8 @@ export type HomeFeedResponse = {
|
||||
export const MODULE_META: Record<HomeModuleId, Omit<HomeModule, "count" | "notifications">> = {
|
||||
suggestions: { id: "suggestions", label: "Today's Queue", href: "/suggestions", accent: "orange" },
|
||||
missions: { id: "missions", label: "Missions", href: "/missions", accent: "orange" },
|
||||
social: { id: "social", label: "Social Branding", href: "/social", accent: "blue" },
|
||||
pathways: { id: "pathways", label: "Pathways", href: "/pathways", accent: "teal" },
|
||||
social: { id: "social", label: "Social Branding", href: "/agents/social-branding", accent: "blue" },
|
||||
pathways: { id: "pathways", label: "Pathways", href: "/agents/matchmaking", accent: "teal" },
|
||||
productivity: { id: "productivity", label: "Interview · Roleplay · Resume", href: "/agents", accent: "orange" },
|
||||
rewards: { id: "rewards", label: "Rewards", href: "/rewards", accent: "amber" },
|
||||
};
|
||||
@@ -62,9 +62,9 @@ export const ALLOWED_NOTIFICATION_HREFS = new Set([
|
||||
"/missions",
|
||||
"/missions/active",
|
||||
"/missions/available",
|
||||
"/social",
|
||||
"/pathways",
|
||||
"/productivity",
|
||||
"/agents/social-branding",
|
||||
"/agents/matchmaking",
|
||||
"/agents",
|
||||
"/rewards",
|
||||
"/agents/resume",
|
||||
"/agents/interview",
|
||||
|
||||
@@ -10,7 +10,6 @@ const LOG_CONTAINERS = {
|
||||
interview: "interview-service-api-1",
|
||||
roleplay: "roleplay-service-api-1",
|
||||
social: "growqr_social_api",
|
||||
pathways: "pathways-service-api-1",
|
||||
courses: "courses_service-api-1",
|
||||
assessment: "assessment-service-api-1",
|
||||
matchmaking: "matchmaking-service-api-1",
|
||||
|
||||
@@ -10,7 +10,6 @@ export type ServiceId =
|
||||
| "courses-service"
|
||||
| "assessment-service"
|
||||
| "matchmaking-service"
|
||||
| "pathways-service"
|
||||
| "resume-service"
|
||||
| "cover-letter-service"
|
||||
| "qscore-service"
|
||||
@@ -155,30 +154,27 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
discovery: {
|
||||
path: "/upskilling/interview",
|
||||
aliases: ["/agents/interview"],
|
||||
path: "/agents/interview",
|
||||
queryParams: ["fresh"],
|
||||
usage: "Entry screen for role selection and resume of in-progress interview work.",
|
||||
},
|
||||
setup: {
|
||||
path: "/upskilling/interview/setup",
|
||||
aliases: ["/agents/interview/setup"],
|
||||
queryParams: ["role", "type", "from_assignment"],
|
||||
path: "/agents/interview/setup",
|
||||
queryParams: ["role", "type", "from_assignment", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Collects interview role, type, duration, persona, media mode, and personalization consent.",
|
||||
},
|
||||
preview: {
|
||||
path: "/upskilling/interview/preview",
|
||||
aliases: ["/agents/interview/preview"],
|
||||
path: "/agents/interview/preview",
|
||||
queryParams: ["role", "type", "persona", "duration", "difficulty", "media", "vip", "from_assignment", "personalize", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Curator default handoff. The page configures the session and opens the launch overlay.",
|
||||
},
|
||||
feedback: {
|
||||
path: "/upskilling/interview/feedback",
|
||||
queryParams: ["sessionId"],
|
||||
path: "/agents/interview/feedback",
|
||||
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Opens feedback/review for a completed or processing session.",
|
||||
},
|
||||
session: {
|
||||
path: "/v2/service-sessions/interview",
|
||||
path: "/agents/interview/preview",
|
||||
queryParams: ["session_id", "goal", "role", "type"],
|
||||
usage: "Legacy service-session launcher used by service agent results.",
|
||||
},
|
||||
@@ -244,30 +240,27 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
discovery: {
|
||||
path: "/upskilling/roleplay",
|
||||
aliases: ["/agents/roleplay"],
|
||||
path: "/agents/roleplay",
|
||||
queryParams: ["fresh"],
|
||||
usage: "Entry screen for scenario discovery and resume of in-progress roleplay work.",
|
||||
},
|
||||
setup: {
|
||||
path: "/upskilling/roleplay/setup",
|
||||
aliases: ["/agents/roleplay/setup"],
|
||||
queryParams: ["scenario", "scenario_text", "scenario_name", "from_assignment"],
|
||||
path: "/agents/roleplay/setup",
|
||||
queryParams: ["scenario", "scenario_text", "scenario_name", "from_assignment", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Collects roleplay scenario details and stores configure parameters for the builder.",
|
||||
},
|
||||
builder: {
|
||||
path: "/upskilling/roleplay/builder",
|
||||
aliases: ["/agents/roleplay/builder"],
|
||||
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "role", "persona", "duration", "mode", "brief"],
|
||||
path: "/agents/roleplay/builder",
|
||||
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "role", "persona", "duration", "mode", "brief", "vip", "from_assignment_id"],
|
||||
usage: "Curator default handoff for generating or resuming a roleplay plan.",
|
||||
},
|
||||
feedback: {
|
||||
path: "/upskilling/roleplay/feedback",
|
||||
queryParams: ["sessionId"],
|
||||
path: "/agents/roleplay/feedback",
|
||||
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Opens feedback/review for a completed or processing session.",
|
||||
},
|
||||
session: {
|
||||
path: "/v2/service-sessions/roleplay",
|
||||
path: "/agents/roleplay/builder",
|
||||
queryParams: ["session_id", "goal", "role", "type"],
|
||||
usage: "Legacy service-session launcher used by service agent results.",
|
||||
},
|
||||
@@ -330,23 +323,22 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
workspace: {
|
||||
path: "/opportunities/resume",
|
||||
aliases: ["/agents/resume"],
|
||||
path: "/agents/resume",
|
||||
queryParams: ["tab", "section", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Resume hub. Use tab=resumes by default and section to deep-link editor panels.",
|
||||
},
|
||||
editor: {
|
||||
path: "/opportunities/resume/:resumeId",
|
||||
path: "/agents/resume/:resumeId",
|
||||
queryParams: ["section"],
|
||||
usage: "Resume editor for a known resume.",
|
||||
},
|
||||
templates: {
|
||||
path: "/opportunities/resume/templates",
|
||||
path: "/agents/resume/templates",
|
||||
queryParams: [],
|
||||
usage: "Template gallery.",
|
||||
},
|
||||
session: {
|
||||
path: "/v2/service-sessions/resume",
|
||||
path: "/agents/resume",
|
||||
queryParams: ["goal", "role"],
|
||||
usage: "Legacy service-session launcher used by service agent results.",
|
||||
},
|
||||
@@ -404,17 +396,17 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
workspace: {
|
||||
path: "/opportunities/resume",
|
||||
path: "/agents/resume",
|
||||
queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Open with tab=cover-letters to land in the cover-letter list.",
|
||||
},
|
||||
generator: {
|
||||
path: "/opportunities/cover-letter",
|
||||
queryParams: [],
|
||||
usage: "Standalone generation entry point.",
|
||||
path: "/agents/resume",
|
||||
queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Cover-letter generation currently lands in the shared resume workspace with tab=cover-letters.",
|
||||
},
|
||||
editor: {
|
||||
path: "/opportunities/resume/cover-letters/:coverLetterId",
|
||||
path: "/agents/resume/cover-letters/:coverLetterId",
|
||||
queryParams: [],
|
||||
usage: "Cover-letter editor for a known coverLetterId.",
|
||||
},
|
||||
@@ -444,7 +436,7 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
{
|
||||
id: "courses-service",
|
||||
label: "Courses",
|
||||
description: "Create, list, and open upskilling courses.",
|
||||
description: "Create, list, and open learning courses.",
|
||||
category: "learning",
|
||||
enabled: Boolean(config.coursesServiceUrl),
|
||||
featureId: "courses",
|
||||
@@ -466,9 +458,19 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
catalog: {
|
||||
path: "/upskilling/course",
|
||||
path: "/agents/courses",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Course catalog and upskilling entry point.",
|
||||
usage: "Course catalog and learning entry point.",
|
||||
},
|
||||
assigned: {
|
||||
path: "/agents/courses/assigned",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Assigned course rail.",
|
||||
},
|
||||
lesson: {
|
||||
path: "/agents/courses/assigned/:assignmentCode/lesson/:lessonId",
|
||||
queryParams: [],
|
||||
usage: "Assigned course lesson player.",
|
||||
},
|
||||
},
|
||||
usage: "Open catalog for general learning handoffs.",
|
||||
@@ -481,7 +483,7 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
},
|
||||
toolName: "prepare_course_handoff",
|
||||
completionEvents: ["course.started", "course.completed"],
|
||||
qscoreSignals: ["skills.learning", "readiness.upskilling"],
|
||||
qscoreSignals: ["skills.learning", "readiness.course_progress"],
|
||||
usage: "Use for skill-gap tasks that should become learning work.",
|
||||
},
|
||||
usageDocs: ["Call buildServiceLink('courses-service', 'catalog', state) for course handoffs."],
|
||||
@@ -511,7 +513,7 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
assessment: {
|
||||
path: "/upskilling/assessment",
|
||||
path: "/agents/assessments",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Assessment landing and active assessment surface.",
|
||||
},
|
||||
@@ -560,18 +562,12 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
jobs: {
|
||||
path: "/opportunities/job-matching",
|
||||
aliases: ["/pathways"],
|
||||
path: "/agents/matchmaking",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "query", "role", "location"],
|
||||
usage: "Job/opportunity matching dashboard.",
|
||||
},
|
||||
pathways: {
|
||||
path: "/career-pathways",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Career pathways dashboard and pathway list.",
|
||||
},
|
||||
},
|
||||
usage: "Use jobs for immediate matching and pathways for career-plan handoffs.",
|
||||
usage: "Use matchmaking for immediate opportunity matching and Scout-style recommendations.",
|
||||
},
|
||||
curator: {
|
||||
defaultPage: "jobs",
|
||||
@@ -587,79 +583,9 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
},
|
||||
usageDocs: [
|
||||
"Call buildServiceLink('matchmaking-service', 'jobs', state) for job matching.",
|
||||
"Use pathways-service for generated career pathway plans.",
|
||||
"Use the canonical /agents/matchmaking page for Scout-style opportunity review.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "pathways-service",
|
||||
label: "Pathways",
|
||||
description: "Generate, activate, and report on personalized career pathways.",
|
||||
category: "opportunity",
|
||||
enabled: Boolean(config.pathwaysServiceUrl),
|
||||
featureId: "pathways",
|
||||
promptModulePath: "agents/pathways.md",
|
||||
aliases: ["career-pathways-service"],
|
||||
backend: {
|
||||
baseUrl: config.pathwaysServiceUrl,
|
||||
publicUrl: config.pathwaysPublicUrl,
|
||||
healthPath: "/api/v1/health",
|
||||
endpoints: {
|
||||
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
||||
state: endpoint("GET", "/api/state/:clerkId", "Reads pathway state.", "Hydrate pathway dashboard."),
|
||||
profileIngest: endpoint("POST", "/api/v1/profiles/ingest", "Ingests a profile for pathway generation.", "Profile setup."),
|
||||
questionnaire: endpoint("GET", "/api/v1/questionnaires/:userId", "Reads pathway questionnaire.", "Questionnaire resume."),
|
||||
createQuestionnaire: endpoint("POST", "/api/v1/questionnaires", "Creates questionnaire answers.", "Pathway onboarding."),
|
||||
generatePathway: endpoint("POST", "/api/v1/pathways/generate", "Generates a pathway.", "Career-plan generation."),
|
||||
activatePathway: endpoint("POST", "/api/v1/pathways/:pathwayId/activate", "Activates a pathway.", "Commit chosen pathway."),
|
||||
getPathway: endpoint("GET", "/api/v1/pathways/:pathwayId", "Reads pathway details.", "Pathway detail."),
|
||||
weeklyPlan: endpoint("GET", "/api/v1/pathways/:pathwayId/weekly-plan", "Reads weekly plan.", "Planner UI."),
|
||||
report: endpoint("GET", "/api/v1/pathways/:pathwayId/report", "Reads pathway report.", "Progress report."),
|
||||
opportunities: endpoint("GET", "/api/v1/pathways/:pathwayId/opportunities", "Reads pathway opportunities.", "Pathway opportunity recommendations."),
|
||||
a2aTask: endpoint("POST", "/a2a/tasks", "Runs pathway agent tasks.", "Agent/curator orchestrated work."),
|
||||
},
|
||||
usage: "Use for generated pathway plans and recommendation context. The live container is currently healthchecked separately from matchmaking.",
|
||||
},
|
||||
frontend: {
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
dashboard: {
|
||||
path: "/career-pathways",
|
||||
aliases: ["/pathways"],
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Pathway list/dashboard.",
|
||||
},
|
||||
generate: {
|
||||
path: "/career-pathways/generate",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Pathway generation flow.",
|
||||
},
|
||||
questionnaire: {
|
||||
path: "/career-pathways/questionnaire",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Pathway questionnaire flow.",
|
||||
},
|
||||
detail: {
|
||||
path: "/career-pathways/dashboard/:pathwayId",
|
||||
queryParams: [],
|
||||
usage: "Pathway detail dashboard.",
|
||||
},
|
||||
},
|
||||
usage: "Use dashboard for general pathway handoffs and generate/questionnaire for guided setup.",
|
||||
},
|
||||
curator: {
|
||||
defaultPage: "dashboard",
|
||||
defaultActionLabel: "Open pathways",
|
||||
actionLabels: {
|
||||
start: "Build pathway",
|
||||
review: "Review pathway",
|
||||
},
|
||||
toolName: "prepare_pathways_handoff",
|
||||
completionEvents: ["pathway.generated", "pathway.activated", "pathway.report_viewed"],
|
||||
qscoreSignals: ["readiness.pathway", "market.plan"],
|
||||
usage: "Use for strategic career pathway planning rather than immediate job matching.",
|
||||
},
|
||||
usageDocs: ["Call buildServiceLink('pathways-service', 'dashboard', state) for pathway handoffs."],
|
||||
},
|
||||
{
|
||||
id: "qscore-service",
|
||||
label: "QScore",
|
||||
@@ -685,13 +611,12 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
dashboard: {
|
||||
path: "/home",
|
||||
aliases: ["/analytics"],
|
||||
path: "/agents/qscore",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Home dashboard with QScore panel.",
|
||||
usage: "Dedicated Q Score service page with live readiness signals.",
|
||||
},
|
||||
},
|
||||
usage: "Open home dashboard for QScore review until a dedicated analytics route exists.",
|
||||
usage: "Open the dedicated Q Score service page for score review.",
|
||||
},
|
||||
curator: {
|
||||
defaultPage: "dashboard",
|
||||
@@ -732,8 +657,7 @@ const serviceRegistry: ServiceRecord[] = [
|
||||
baseUrl: frontendBaseUrl,
|
||||
pages: {
|
||||
profile: {
|
||||
path: "/opportunities/social-media",
|
||||
aliases: ["/social"],
|
||||
path: "/agents/social-branding",
|
||||
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
||||
usage: "Social/profile improvement page.",
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray } from "drizzle-orm";
|
||||
import { db } from "../../db/client.js";
|
||||
import { growEvents, growHomeNotifications, type GrowEventRow } from "../../db/schema.js";
|
||||
import { asRecord, getString } from "../../events/envelope.js";
|
||||
@@ -44,6 +44,17 @@ function parseCompletedAt(value: unknown): string | undefined {
|
||||
return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
|
||||
}
|
||||
|
||||
function onboardingContextFromPayload(payload: Record<string, unknown>) {
|
||||
const preferences = asRecord(payload.preferences);
|
||||
const onboarding = asRecord(payload.onboarding ?? preferences.onboarding);
|
||||
if (!Object.keys(onboarding).length && !Object.keys(preferences).length) return undefined;
|
||||
return {
|
||||
onboarding,
|
||||
preferences: Object.keys(preferences).length ? preferences : { onboarding },
|
||||
source: "grow_events",
|
||||
};
|
||||
}
|
||||
|
||||
export function onboardingCompletedAtFromPreferences(preferences: Record<string, unknown> | undefined) {
|
||||
const onboarding = asRecord(preferences?.onboarding);
|
||||
return parseCompletedAt(onboarding.completed_at ?? onboarding.completedAt);
|
||||
@@ -123,6 +134,22 @@ async function recordOnboardingContextSnapshot(input: {
|
||||
}, { userId: input.userId, source: input.source ?? "onboarding" });
|
||||
}
|
||||
|
||||
async function findLatestOnboardingContext(userId: string) {
|
||||
const [event] = await db
|
||||
.select({ id: growEvents.id, payload: growEvents.payload, occurredAt: growEvents.occurredAt, source: growEvents.source, type: growEvents.type })
|
||||
.from(growEvents)
|
||||
.where(and(
|
||||
eq(growEvents.userId, userId),
|
||||
inArray(growEvents.type, ["onboarding.snapshot.saved", "onboarding.completed", "user.onboarding.completed", "profile.onboarding.completed"]),
|
||||
))
|
||||
.orderBy(desc(growEvents.occurredAt))
|
||||
.limit(1);
|
||||
|
||||
if (!event?.payload) return undefined;
|
||||
const context = onboardingContextFromPayload(event.payload);
|
||||
return context ? { ...context, sourceEventId: event.id, sourceEventType: event.type, sourceEventSource: event.source } : undefined;
|
||||
}
|
||||
|
||||
async function fetchUserServiceContext(userId: string): Promise<Record<string, unknown> | undefined> {
|
||||
const token = config.serviceToken || (config.nodeEnv !== "production" ? config.a2aAllowedKey : "");
|
||||
if (!token) return undefined;
|
||||
@@ -228,7 +255,7 @@ export async function runCuratorOnboardingLoop(input: OnboardingLoopInput): Prom
|
||||
}
|
||||
|
||||
const startDate = isoDateFrom(input.completedAt);
|
||||
const context = input.context ?? await fetchUserServiceContext(userId);
|
||||
const context = input.context ?? await findLatestOnboardingContext(userId) ?? await fetchUserServiceContext(userId);
|
||||
await recordOnboardingContextSnapshot({
|
||||
userId,
|
||||
startDate,
|
||||
@@ -272,6 +299,7 @@ export async function runCuratorOnboardingLoopForEvent(event: GrowEventRow): Pro
|
||||
completedAt: onboardingCompletedAtFromEvent(event),
|
||||
sourceEventId: event.id,
|
||||
source: event.source,
|
||||
context: onboardingContextFromPayload(event.payload ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { eq, desc, and, inArray } from "drizzle-orm";
|
||||
import { db } from "../../db/client.js";
|
||||
import { growEvents, growQscoreLatest, growQscoreProjectionState } from "../../db/schema.js";
|
||||
import { interviewService, resumeService, roleplayService } from "../../services/product-service-clients.js";
|
||||
import { listServices } from "../../services/service-registry.js";
|
||||
import { createMissionAction, listMissionActions } from "../../missions/actions.js";
|
||||
import { listActiveMissionsPg, listMessagesPg } from "../../grow/persistence.js";
|
||||
import { buildCuratorStreak, buildCuratorTasks, listCuratorRegistryCapabilities } from "./curator-store.js";
|
||||
@@ -276,9 +277,9 @@ export function buildCuratorTools(ctx: { userId: string; date: string; conversat
|
||||
execute: async ({ taskId }) => {
|
||||
const task = taskId ? await findTask(ctx.userId, taskId, ctx.date) : null;
|
||||
return {
|
||||
routes: ["interview-service", "resume-service", "roleplay-service", "qscore-service"].map((serviceId) => ({
|
||||
serviceId,
|
||||
route: serviceRoute({ serviceId: serviceId as any, missionId: task?.missionId, missionInstanceId: task?.missionInstanceId, stageId: task?.stageId, taskId: task?.id }),
|
||||
routes: listServices().map((service) => ({
|
||||
serviceId: service.id,
|
||||
route: serviceRoute({ serviceId: service.id, missionId: task?.missionId, missionInstanceId: task?.missionInstanceId, stageId: task?.stageId, taskId: task?.id }),
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ export const curatorServiceIdSchema = z.enum([
|
||||
"qscore-service",
|
||||
"social-branding-service",
|
||||
"matchmaking-service",
|
||||
"pathways-service",
|
||||
]);
|
||||
|
||||
export type CuratorServiceId = z.infer<typeof curatorServiceIdSchema>;
|
||||
|
||||
Reference in New Issue
Block a user