evOo-local/apps/web/src/lib/onboarding-discovery.ts
cynarAI 97787a0272
Production readiness phase 1: journey audit, fixes 1–4, E2E (#175)
* Production readiness phase 1: journeys doc, onboarding persistence, teardown, chat UX

- Add docs/PRODUCTION-READINESS.md with journey matrix and defect backlog
- Persist onboarding draft in session storage across reload
- Localize onboarding discovery main-bot role; German Chief of Staff string
- Destroy team sandboxes and home folders when deleting an account
- Keep composer/transcript visible during queued runs; extend send watchdog
- Add Playwright specs: reload persistence, DE intake, queued progress, console guard

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix CI: sync Lingui catalogs, onboarding name draft, E2E stability

- Localize Chief of Staff in discovery shell; sync all locale PO files
- Propagate Oo name to onboarding draft while typing
- Adjust DE intake, queued-run, and console-error E2E specs

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Format chat-queued-progress E2E spec

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(api): scope account-delete sandbox teardown to owned spaces

Only destroy team sandboxes and homes for spaces the deleting user
owns with no remaining members or bots. Guard home removal under
dataDir/homes. Add unit and postgres journey coverage.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* chore: apply biome formatting

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 11:25:39 +02:00

114 lines
3.8 KiB
TypeScript

import type { IntegrationSetupState, Me } from "@rakazo/contracts";
import { collectLocalProfileSignals, type UserProfileSignals } from "./user-profile-harvest";
/** Sentinel stored in discovery `detail`; the shell renders it with Lingui. */
export const DISCOVERY_MAIN_BOT_ROLE = "main-bot-role";
export type DiscoverySourceId = "profile-basics" | "connected-tools" | "specialist-crew";
export type DiscoveryStatus = "pending" | "scanning" | "found" | "skipped";
export interface DiscoveryItem {
id: DiscoverySourceId;
status: DiscoveryStatus;
detail?: string;
}
export type OnboardingDiscoveryPhase = "loading" | "emoji" | "integrations" | "assembly";
export interface DiscoveryContext {
phase: OnboardingDiscoveryPhase;
me?: Pick<
Me,
"name" | "email" | "needsModel" | "defaultProvider" | "defaultModel" | "computerHost"
> | null;
integrationSetup?: IntegrationSetupState | null;
connectedIntegrationCount?: number;
profileSignals?: UserProfileSignals;
profilePersisted?: boolean;
crewReady?: boolean;
}
export function summarizeProfileSignals(signals: UserProfileSignals): string {
const parts: string[] = [];
if (signals.name) parts.push(signals.name);
parts.push(signals.timezone);
if (signals.uiLocale && signals.uiLocale !== "en") parts.push(signals.uiLocale);
return parts.join(" · ");
}
export function countConfiguredIntegrations(
setup: IntegrationSetupState | null | undefined,
): number {
return setup?.providers.filter((provider) => provider.configured).length ?? 0;
}
export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[] {
const signals =
context.profileSignals ?? (context.me ? collectLocalProfileSignals(context.me) : null);
const configuredCount =
context.connectedIntegrationCount ?? countConfiguredIntegrations(context.integrationSetup);
const profile: DiscoveryItem = (() => {
if (context.profilePersisted && signals) {
return { id: "profile-basics", status: "found", detail: summarizeProfileSignals(signals) };
}
if (context.phase === "assembly" && signals) {
return { id: "profile-basics", status: "scanning", detail: summarizeProfileSignals(signals) };
}
if (signals && context.me) {
return { id: "profile-basics", status: "found", detail: summarizeProfileSignals(signals) };
}
if (context.phase === "loading") {
return { id: "profile-basics", status: "scanning" };
}
return { id: "profile-basics", status: "pending" };
})();
const tools: DiscoveryItem = (() => {
if (configuredCount > 0) {
return {
id: "connected-tools",
status: "found",
detail: String(configuredCount),
};
}
if (context.phase === "integrations") {
return { id: "connected-tools", status: "scanning" };
}
if (context.integrationSetup && !context.integrationSetup.needsSetup) {
return { id: "connected-tools", status: "skipped" };
}
return { id: "connected-tools", status: "pending" };
})();
const crew: DiscoveryItem = (() => {
if (context.crewReady) {
return { id: "specialist-crew", status: "found", detail: DISCOVERY_MAIN_BOT_ROLE };
}
if (context.phase === "assembly") {
return { id: "specialist-crew", status: "scanning" };
}
return { id: "specialist-crew", status: "pending" };
})();
return [profile, tools, crew];
}
export function visibleDiscoveryItems(items: DiscoveryItem[]): DiscoveryItem[] {
return items;
}
export function discoveryProgress(items: DiscoveryItem[]): {
done: number;
total: number;
active: DiscoveryItem | null;
} {
const done = items.filter((item) => item.status === "found" || item.status === "skipped").length;
const active =
items.find((item) => item.status === "scanning") ??
items.find((item) => item.status === "pending") ??
null;
return { done, total: items.length, active };
}