@burdenoff/microfe-planmagnet 2026.1001.5 → 2026.1001.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoutes.js +10 -6
  3. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  4. package/dist/planmagnet/automation/automationRuleTypes.d.ts +31 -19
  5. package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -1
  6. package/dist/planmagnet/automation/automationRuleTypes.js +4 -8
  7. package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -1
  8. package/dist/planmagnet/controls/CompetitorControls.d.ts +71 -0
  9. package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/CompetitorControls.js +346 -12
  11. package/dist/planmagnet/controls/CompetitorControls.js.map +1 -1
  12. package/dist/planmagnet/controls/FeatureControls.d.ts +23 -1
  13. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  14. package/dist/planmagnet/controls/FeatureControls.js +431 -229
  15. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  16. package/dist/planmagnet/controls/FeedbackControls.d.ts +70 -0
  17. package/dist/planmagnet/controls/FeedbackControls.d.ts.map +1 -0
  18. package/dist/planmagnet/controls/FeedbackControls.js +153 -0
  19. package/dist/planmagnet/controls/FeedbackControls.js.map +1 -0
  20. package/dist/planmagnet/controls/Pickers.d.ts +15 -0
  21. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  22. package/dist/planmagnet/controls/Pickers.js +61 -1
  23. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  24. package/dist/planmagnet/controls/PrioritizationControls.d.ts +36 -0
  25. package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -1
  26. package/dist/planmagnet/controls/PrioritizationControls.js +118 -1
  27. package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -1
  28. package/dist/planmagnet/controls/SyncControls.d.ts.map +1 -1
  29. package/dist/planmagnet/controls/SyncControls.js +1 -0
  30. package/dist/planmagnet/controls/SyncControls.js.map +1 -1
  31. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts +47 -0
  32. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts.map +1 -0
  33. package/dist/planmagnet/controls/WebhookSubscriptionControls.js +542 -0
  34. package/dist/planmagnet/controls/WebhookSubscriptionControls.js.map +1 -0
  35. package/dist/planmagnet/dashboards/DashboardUI.js +8 -8
  36. package/dist/planmagnet/dashboards/cards/RiskSeverityCard.js +4 -4
  37. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
  38. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +67 -59
  39. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
  40. package/dist/planmagnet/forecast/DeliveryForecastCard.js +8 -8
  41. package/dist/planmagnet/forecast/ProjectOutlookCard.js +10 -10
  42. package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +9 -9
  43. package/dist/planmagnet/forecast/ResourceForecastCard.js +6 -6
  44. package/dist/planmagnet/integrations/syncTypes.d.ts +2 -1
  45. package/dist/planmagnet/integrations/syncTypes.d.ts.map +1 -1
  46. package/dist/planmagnet/integrations/syncTypes.js +19 -9
  47. package/dist/planmagnet/integrations/syncTypes.js.map +1 -1
  48. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
  49. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +290 -283
  50. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  51. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -1
  52. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +126 -63
  53. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -1
  54. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -1
  55. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +14 -0
  56. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -1
  57. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -1
  58. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +93 -57
  59. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -1
  60. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -1
  61. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +125 -83
  62. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -1
  63. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +23 -23
  64. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  65. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +340 -248
  66. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  67. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.d.ts.map +1 -1
  68. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js +172 -167
  69. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js.map +1 -1
  70. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.d.ts.map +1 -1
  71. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +92 -83
  72. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js.map +1 -1
  73. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts +2 -0
  74. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts.map +1 -0
  75. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js +175 -0
  76. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js.map +1 -0
  77. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
  78. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +226 -126
  79. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
  80. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  81. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +151 -126
  82. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  83. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  84. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +169 -122
  85. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  86. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  87. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +165 -109
  88. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  89. package/dist/planmagnet/pages/ProductsProductsPagePage.js +13 -13
  90. package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +9 -9
  91. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +8 -8
  92. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  93. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +15 -3
  94. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  95. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +12 -12
  96. package/dist/planmagnet/pages/ReportsReportsPagePage.js +23 -23
  97. package/dist/planmagnet/pages/TasksMyDashboardPagePage.js +18 -18
  98. package/dist/planmagnet/pages/TasksMyWorkPagePage.js +10 -10
  99. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +5 -5
  100. package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
  101. package/dist/planmagnet/pages/TeamTeamPagePage.js +59 -53
  102. package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
  103. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -8
  104. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +6 -6
  105. package/dist/planmagnet/pages/analytics/MetricsCard.d.ts +8 -0
  106. package/dist/planmagnet/pages/analytics/MetricsCard.d.ts.map +1 -0
  107. package/dist/planmagnet/pages/analytics/MetricsCard.js +164 -0
  108. package/dist/planmagnet/pages/analytics/MetricsCard.js.map +1 -0
  109. package/dist/planmagnet/pages/analytics/QualityMetricsCard.js +6 -6
  110. package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +8 -8
  111. package/dist/planmagnet/pages/analytics/StatusFlowCard.js +7 -7
  112. package/dist/planmagnet/pages/analytics/ThroughputCard.js +7 -7
  113. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +7 -7
  114. package/dist/planmagnet/people/People.d.ts +14 -0
  115. package/dist/planmagnet/people/People.d.ts.map +1 -1
  116. package/dist/planmagnet/people/People.js +36 -22
  117. package/dist/planmagnet/people/People.js.map +1 -1
  118. package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts +2 -0
  119. package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts.map +1 -0
  120. package/dist/planmagnet/team/UnmatchedCrewIdentities.js +155 -0
  121. package/dist/planmagnet/team/UnmatchedCrewIdentities.js.map +1 -0
  122. package/dist/planmagnet/webhooks/webhookTypes.d.ts +54 -0
  123. package/dist/planmagnet/webhooks/webhookTypes.d.ts.map +1 -0
  124. package/dist/planmagnet/webhooks/webhookTypes.js +17 -0
  125. package/dist/planmagnet/webhooks/webhookTypes.js.map +1 -0
  126. package/package.json +1 -1
@@ -1,48 +1,62 @@
1
1
  import { createContext as e, useCallback as t, useContext as n, useEffect as r, useMemo as i, useState as a } from "react";
2
2
  import { jsx as o } from "react/jsx-runtime";
3
3
  import { buildActorFallback as s, useActorProfiles as c } from "@burdenoff/fe-libs/shared";
4
+ import { Badge as l } from "@burdenoff/fe-libs/ui";
4
5
  //#region src/planmagnet/people/People.tsx
5
- var l = {
6
+ var u = {
6
7
  "automation:engine": "Automation",
7
8
  "automation:scheduler": "Automation"
8
- }, u = e(null);
9
- function d({ workspaceId: e, children: n }) {
9
+ }, d = "anonymous";
10
+ function f(e) {
11
+ return e === d;
12
+ }
13
+ function p({ className: e }) {
14
+ return /* @__PURE__ */ o(l, {
15
+ variant: "secondary",
16
+ className: e ?? "text-xs",
17
+ "data-testid": "anonymous-badge",
18
+ children: "Anonymous"
19
+ });
20
+ }
21
+ var m = e(null);
22
+ function h({ workspaceId: e, children: n }) {
10
23
  let [r, s] = a([]), l = t((e) => {
11
24
  s((t) => t.includes(e) ? t : [...t, e]);
12
- }, []), d = c(i(() => r.map((e) => ({
25
+ }, []), u = c(i(() => r.map((e) => ({
13
26
  actorId: e,
14
27
  actorType: "user"
15
- })), [r]), { workspaceId: e }), f = i(() => ({
28
+ })), [r]), { workspaceId: e }), d = i(() => ({
16
29
  request: l,
17
- profiles: d
18
- }), [l, d]);
19
- return /* @__PURE__ */ o(u.Provider, {
20
- value: f,
30
+ profiles: u
31
+ }), [l, u]);
32
+ return /* @__PURE__ */ o(m.Provider, {
33
+ value: d,
21
34
  children: n
22
35
  });
23
36
  }
24
- function f(e) {
25
- let t = n(u), i = t?.request, a = e ? l[e] : void 0;
37
+ function g(e) {
38
+ let t = n(m), i = t?.request, a = e ? u[e] : void 0, o = f(e);
26
39
  return r(() => {
27
- e && i && !a && i(e);
40
+ e && i && !a && !o && i(e);
28
41
  }, [
29
42
  e,
30
43
  i,
31
- a
44
+ a,
45
+ o
32
46
  ]), e ? a ? {
33
47
  actorId: e,
34
48
  actorType: "service",
35
49
  displayName: a,
36
50
  secondaryLabel: "Automation",
37
51
  resolved: !0
38
- } : t?.profiles[e] ?? s({
52
+ } : o ? null : t?.profiles[e] ?? s({
39
53
  actorId: e,
40
54
  actorType: "user"
41
55
  }) : null;
42
56
  }
43
- function p({ id: e, className: t }) {
44
- let n = f(e);
45
- return n ? /* @__PURE__ */ o("span", {
57
+ function _({ id: e, className: t }) {
58
+ let n = g(e);
59
+ return f(e) ? /* @__PURE__ */ o(p, { className: t }) : n ? /* @__PURE__ */ o("span", {
46
60
  className: t,
47
61
  title: n.email ?? void 0,
48
62
  children: n.displayName
@@ -51,19 +65,19 @@ function p({ id: e, className: t }) {
51
65
  children: "—"
52
66
  });
53
67
  }
54
- function m(e) {
68
+ function v(e) {
55
69
  return e.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((e) => e[0].toUpperCase()).join("");
56
70
  }
57
- function h({ id: e, className: t }) {
58
- let n = f(e), r = n ? n.email ? `${n.displayName} (${n.email})` : n.displayName : void 0;
71
+ function y({ id: e, className: t }) {
72
+ let n = g(e), r = n ? n.email ? `${n.displayName} (${n.email})` : n.displayName : void 0;
59
73
  return /* @__PURE__ */ o("span", {
60
74
  className: t,
61
75
  title: r,
62
76
  "aria-label": r,
63
- children: (n?.resolved ? m(n.displayName) : "") || "?"
77
+ children: (n?.resolved ? v(n.displayName) : "") || "?"
64
78
  });
65
79
  }
66
80
  //#endregion
67
- export { d as PeopleProvider, h as PersonAvatar, p as PersonName, m as initialsOf, f as usePersonProfile };
81
+ export { h as PeopleProvider, y as PersonAvatar, _ as PersonName, v as initialsOf, g as usePersonProfile };
68
82
 
69
83
  //# sourceMappingURL=People.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"People.js","names":[],"sources":["../../../src/planmagnet/people/People.tsx"],"sourcesContent":["import {\n buildActorFallback,\n useActorProfiles,\n type ActorInput,\n type ActorProfile,\n type ActorProfileMap,\n} from '@burdenoff/fe-libs/shared';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\n/**\n * Show people by name, not by UUID.\n *\n * Until 2026-09-24, 21 places in this MFE rendered a raw user id where a\n * person belonged — project owners, roadmap owners, assignees, the team lead.\n * fe-libs already resolves ids to workspace-member identities\n * (`useActorProfiles`, with a 5-minute cache), but it batches per CALL and\n * does not dedupe across calls, so calling it from every row would fire a\n * burst of lookups on the first render of each list.\n *\n * So there is exactly one call, here. Every `<PersonName id>` on screen\n * registers its id with the provider; ids registered during the same render\n * land in one state update, and the provider resolves them together. A\n * growing list costs one request for the NEW ids only — already-resolved ids\n * come from the fe-libs cache.\n *\n * An id that cannot be resolved (not a member of this workspace, lookup\n * failed) renders fe-libs' short fallback, \"User 1a2b3c\", rather than the full\n * UUID: still identifiable, never a wall of hex.\n */\n\ninterface PeopleContextValue {\n request: (id: string) => void;\n profiles: ActorProfileMap;\n}\n\n/**\n * The automation engine's own actor identities (BOFF-8047 follow-up) — never a real workspace member's id, so\n * they are mapped explicitly rather than sent through `useActorProfiles` (which would only ever resolve them to\n * its generic \"not a member\" fallback). Sourced directly from wspace-planmagnet-svc, not guessed:\n * - `automation-engine.ts`'s `AUTOMATION_COMMENT_AUTHOR_ID` (`'automation:engine'`) — the fixed author of every\n * comment a POST_COMMENT action posts, regardless of which trigger or actor caused the run.\n * - `automation-scheduler.ts`'s `AUTOMATION_SCHEDULER_ACTOR_ID` (`'automation:scheduler'`) — the actor behind a\n * SCHEDULE-triggered run (threaded through as `updatedBy` on that run's other actions).\n * No real actor id can ever collide with the `automation:` prefix (workspace-member ids are uuids) — see that\n * constant's own doc comment in the engine.\n */\nconst AUTOMATION_ACTOR_NAMES: Record<string, string> = {\n 'automation:engine': 'Automation',\n 'automation:scheduler': 'Automation',\n};\n\nconst PeopleContext = createContext<PeopleContextValue | null>(null);\n\nexport function PeopleProvider({\n workspaceId,\n children,\n}: {\n workspaceId?: string;\n children: ReactNode;\n}) {\n const [ids, setIds] = useState<readonly string[]>([]);\n\n const request = useCallback((id: string) => {\n setIds((previous) => (previous.includes(id) ? previous : [...previous, id]));\n }, []);\n\n const actors = useMemo<ActorInput[]>(\n () => ids.map((actorId) => ({ actorId, actorType: 'user' })),\n [ids]\n );\n\n // No apiGatewayUrl on purpose: this MFE is handed the WORKSPACE gateway, and\n // the hook resolves the right gateways from the hostname itself.\n const profiles = useActorProfiles(actors, { workspaceId });\n\n const value = useMemo(() => ({ request, profiles }), [request, profiles]);\n return <PeopleContext.Provider value={value}>{children}</PeopleContext.Provider>;\n}\n\nexport interface PersonNameProps {\n /** A user id. Empty or missing renders an em dash. */\n id: string | null | undefined;\n className?: string;\n}\n\n/**\n * Inline text on purpose. fe-libs' `ActorIdentity` is a block (avatar plus two\n * stacked paragraphs); every place this replaces is an inline label —\n * \"Owner: …\", a table cell, a <span> inside a <p> — where a block would break\n * the layout and nest a <div> inside a <p>. The email, when known, is the\n * hover title, so two people with the same name stay distinguishable.\n */\n/**\n * Register an id with the provider and return its profile: the resolved identity, or fe-libs' short fallback\n * (\"User 1a2b3c\", `resolved: false`) until and unless it resolves. `null` for an empty id.\n */\nexport function usePersonProfile(id: string | null | undefined): ActorProfile | null {\n const context = useContext(PeopleContext);\n const request = context?.request;\n const automationName = id ? AUTOMATION_ACTOR_NAMES[id] : undefined;\n\n useEffect(() => {\n // An automation identity is never a real workspace member — registering it with the provider would only\n // ever come back as the generic \"not a member\" fallback, and would cost a wasted slot in every batch lookup.\n if (id && request && !automationName) request(id);\n }, [id, request, automationName]);\n\n if (!id) return null;\n if (automationName) {\n return {\n actorId: id,\n actorType: 'service',\n displayName: automationName,\n secondaryLabel: 'Automation',\n resolved: true,\n };\n }\n return context?.profiles[id] ?? buildActorFallback({ actorId: id, actorType: 'user' });\n}\n\nexport function PersonName({ id, className }: PersonNameProps) {\n const profile = usePersonProfile(id);\n if (!profile) return <span className={className ?? 'text-muted-foreground'}>—</span>;\n return (\n <span className={className} title={profile.email ?? undefined}>\n {profile.displayName}\n </span>\n );\n}\n\n/** Up to two initials from a display name (\"Ada Lovelace\" → \"AL\", \"ada\" → \"A\"). */\nexport function initialsOf(name: string): string {\n return name\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((word) => word[0]!.toUpperCase())\n .join('');\n}\n\nexport interface PersonAvatarProps {\n id: string | null | undefined;\n className?: string;\n}\n\n/**\n * An initials bubble for a user id. Initials come from the RESOLVED name; until the id resolves (or when it never\n * does) the bubble shows a neutral \"?\" rather than letters cut from the UUID, which matched nobody (BOFF-7977,\n * BOFF-8000). The tooltip is the name, plus the email when known.\n */\nexport function PersonAvatar({ id, className }: PersonAvatarProps) {\n const profile = usePersonProfile(id);\n const label = profile\n ? profile.email\n ? `${profile.displayName} (${profile.email})`\n : profile.displayName\n : undefined;\n const initials = profile?.resolved ? initialsOf(profile.displayName) : '';\n return (\n <span className={className} title={label} aria-label={label}>\n {initials || '?'}\n </span>\n );\n}\n"],"mappings":";;;;AAsDA,IAAM,IAAiD;CACrD,qBAAqB;CACrB,wBAAwB;CACzB,EAEK,IAAgB,EAAyC,KAAK;AAEpE,SAAgB,EAAe,EAC7B,gBACA,eAIC;CACD,IAAM,CAAC,GAAK,KAAU,EAA4B,EAAE,CAAC,EAE/C,IAAU,GAAa,MAAe;AAC1C,KAAQ,MAAc,EAAS,SAAS,EAAG,GAAG,IAAW,CAAC,GAAG,GAAU,EAAG,CAAE;IAC3E,EAAE,CAAC,EASA,IAAW,EAPF,QACP,EAAI,KAAK,OAAa;EAAE;EAAS,WAAW;EAAQ,EAAE,EAC5D,CAAC,EAAI,CACN,EAIyC,EAAE,gBAAa,CAAC,EAEpD,IAAQ,SAAe;EAAE;EAAS;EAAU,GAAG,CAAC,GAAS,EAAS,CAAC;AACzE,QAAO,kBAAC,EAAc,UAAf;EAA+B;EAAQ;EAAkC,CAAA;;AAoBlF,SAAgB,EAAiB,GAAoD;CACnF,IAAM,IAAU,EAAW,EAAc,EACnC,IAAU,GAAS,SACnB,IAAiB,IAAK,EAAuB,KAAM,KAAA;AAkBzD,QAhBA,QAAgB;AAGd,EAAI,KAAM,KAAW,CAAC,KAAgB,EAAQ,EAAG;IAChD;EAAC;EAAI;EAAS;EAAe,CAAC,EAE5B,IACD,IACK;EACL,SAAS;EACT,WAAW;EACX,aAAa;EACb,gBAAgB;EAChB,UAAU;EACX,GAEI,GAAS,SAAS,MAAO,EAAmB;EAAE,SAAS;EAAI,WAAW;EAAQ,CAAC,GAVtE;;AAalB,SAAgB,EAAW,EAAE,OAAI,gBAA8B;CAC7D,IAAM,IAAU,EAAiB,EAAG;AAEpC,QADK,IAEH,kBAAC,QAAD;EAAiB;EAAW,OAAO,EAAQ,SAAS,KAAA;YACjD,EAAQ;EACJ,CAAA,GAJY,kBAAC,QAAD;EAAM,WAAW,KAAa;YAAyB;EAAQ,CAAA;;AAStF,SAAgB,EAAW,GAAsB;AAC/C,QAAO,EACJ,MAAM,CACN,MAAM,MAAM,CACZ,OAAO,QAAQ,CACf,MAAM,GAAG,EAAE,CACX,KAAK,MAAS,EAAK,GAAI,aAAa,CAAC,CACrC,KAAK,GAAG;;AAab,SAAgB,EAAa,EAAE,OAAI,gBAAgC;CACjE,IAAM,IAAU,EAAiB,EAAG,EAC9B,IAAQ,IACV,EAAQ,QACN,GAAG,EAAQ,YAAY,IAAI,EAAQ,MAAM,KACzC,EAAQ,cACV,KAAA;AAEJ,QACE,kBAAC,QAAD;EAAiB;EAAW,OAAO;EAAO,cAAY;aAFvC,GAAS,WAAW,EAAW,EAAQ,YAAY,GAAG,OAGtD;EACR,CAAA"}
1
+ {"version":3,"file":"People.js","names":[],"sources":["../../../src/planmagnet/people/People.tsx"],"sourcesContent":["import {\n buildActorFallback,\n useActorProfiles,\n type ActorInput,\n type ActorProfile,\n type ActorProfileMap,\n} from '@burdenoff/fe-libs/shared';\nimport { Badge } from '@burdenoff/fe-libs/ui';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\n/**\n * Show people by name, not by UUID.\n *\n * Until 2026-09-24, 21 places in this MFE rendered a raw user id where a\n * person belonged — project owners, roadmap owners, assignees, the team lead.\n * fe-libs already resolves ids to workspace-member identities\n * (`useActorProfiles`, with a 5-minute cache), but it batches per CALL and\n * does not dedupe across calls, so calling it from every row would fire a\n * burst of lookups on the first render of each list.\n *\n * So there is exactly one call, here. Every `<PersonName id>` on screen\n * registers its id with the provider; ids registered during the same render\n * land in one state update, and the provider resolves them together. A\n * growing list costs one request for the NEW ids only — already-resolved ids\n * come from the fe-libs cache.\n *\n * An id that cannot be resolved (not a member of this workspace, lookup\n * failed) renders fe-libs' short fallback, \"User 1a2b3c\", rather than the full\n * UUID: still identifiable, never a wall of hex.\n */\n\ninterface PeopleContextValue {\n request: (id: string) => void;\n profiles: ActorProfileMap;\n}\n\n/**\n * The automation engine's own actor identities (BOFF-8047 follow-up) — never a real workspace member's id, so\n * they are mapped explicitly rather than sent through `useActorProfiles` (which would only ever resolve them to\n * its generic \"not a member\" fallback). Sourced directly from wspace-planmagnet-svc, not guessed:\n * - `automation-engine.ts`'s `AUTOMATION_COMMENT_AUTHOR_ID` (`'automation:engine'`) — the fixed author of every\n * comment a POST_COMMENT action posts, regardless of which trigger or actor caused the run.\n * - `automation-scheduler.ts`'s `AUTOMATION_SCHEDULER_ACTOR_ID` (`'automation:scheduler'`) — the actor behind a\n * SCHEDULE-triggered run (threaded through as `updatedBy` on that run's other actions).\n * No real actor id can ever collide with the `automation:` prefix (workspace-member ids are uuids) — see that\n * constant's own doc comment in the engine.\n */\nconst AUTOMATION_ACTOR_NAMES: Record<string, string> = {\n 'automation:engine': 'Automation',\n 'automation:scheduler': 'Automation',\n};\n\n/**\n * CWF-15's redaction sentinel: `FeatureRequest.submittedBy`/`createdBy`/`updatedBy` and `FeatureVote.voterId`\n * read back this literal string, instead of the real actor id, for a non-moderator caller when the record (or,\n * for a vote, that vote) is flagged `isAnonymous`. Never a real actor id — those are workspace-member uuids —\n * so, like the `automation:` actor ids above, it must never be sent through `useActorProfiles`: that would only\n * ever resolve to the generic \"not a member\" fallback, and would burn a wasted slot in every batch lookup.\n */\nexport const ANONYMOUS_ACTOR_ID = 'anonymous';\n\nexport function isAnonymousActorId(id: string | null | undefined): boolean {\n return id === ANONYMOUS_ACTOR_ID;\n}\n\n/** The literal \"Anonymous\" badge CWF-15 asks for everywhere a redacted actor id would otherwise show — see\n * `PersonName` below, which is the one place `submittedBy`/`createdBy`/`updatedBy`/`voterId` ever render. */\nexport function AnonymousBadge({ className }: { className?: string }) {\n return (\n <Badge variant=\"secondary\" className={className ?? 'text-xs'} data-testid=\"anonymous-badge\">\n Anonymous\n </Badge>\n );\n}\n\nconst PeopleContext = createContext<PeopleContextValue | null>(null);\n\nexport function PeopleProvider({\n workspaceId,\n children,\n}: {\n workspaceId?: string;\n children: ReactNode;\n}) {\n const [ids, setIds] = useState<readonly string[]>([]);\n\n const request = useCallback((id: string) => {\n setIds((previous) => (previous.includes(id) ? previous : [...previous, id]));\n }, []);\n\n const actors = useMemo<ActorInput[]>(\n () => ids.map((actorId) => ({ actorId, actorType: 'user' })),\n [ids]\n );\n\n // No apiGatewayUrl on purpose: this MFE is handed the WORKSPACE gateway, and\n // the hook resolves the right gateways from the hostname itself.\n const profiles = useActorProfiles(actors, { workspaceId });\n\n const value = useMemo(() => ({ request, profiles }), [request, profiles]);\n return <PeopleContext.Provider value={value}>{children}</PeopleContext.Provider>;\n}\n\nexport interface PersonNameProps {\n /** A user id. Empty or missing renders an em dash. */\n id: string | null | undefined;\n className?: string;\n}\n\n/**\n * Inline text on purpose. fe-libs' `ActorIdentity` is a block (avatar plus two\n * stacked paragraphs); every place this replaces is an inline label —\n * \"Owner: …\", a table cell, a <span> inside a <p> — where a block would break\n * the layout and nest a <div> inside a <p>. The email, when known, is the\n * hover title, so two people with the same name stay distinguishable.\n */\n/**\n * Register an id with the provider and return its profile: the resolved identity, or fe-libs' short fallback\n * (\"User 1a2b3c\", `resolved: false`) until and unless it resolves. `null` for an empty id.\n */\nexport function usePersonProfile(id: string | null | undefined): ActorProfile | null {\n const context = useContext(PeopleContext);\n const request = context?.request;\n const automationName = id ? AUTOMATION_ACTOR_NAMES[id] : undefined;\n const anonymous = isAnonymousActorId(id);\n\n useEffect(() => {\n // An automation identity is never a real workspace member, and the CWF-15 \"anonymous\" sentinel is never an\n // actor id at all — registering either with the provider would only ever come back as the generic \"not a\n // member\" fallback, and would cost a wasted slot in every batch lookup.\n if (id && request && !automationName && !anonymous) request(id);\n }, [id, request, automationName, anonymous]);\n\n if (!id) return null;\n if (automationName) {\n return {\n actorId: id,\n actorType: 'service',\n displayName: automationName,\n secondaryLabel: 'Automation',\n resolved: true,\n };\n }\n // CWF-15: \"anonymous\" is a redaction sentinel, never a real actor id — PersonName/PersonAvatar below render it\n // specially rather than resolving it (which would only ever land on the generic \"not a member\" fallback).\n if (anonymous) return null;\n return context?.profiles[id] ?? buildActorFallback({ actorId: id, actorType: 'user' });\n}\n\nexport function PersonName({ id, className }: PersonNameProps) {\n const profile = usePersonProfile(id);\n // CWF-15: the literal \"anonymous\" sentinel renders as the Anonymous badge everywhere a person's name would\n // otherwise show — never resolved as, or displayed like, a real user id.\n if (isAnonymousActorId(id)) return <AnonymousBadge className={className} />;\n if (!profile) return <span className={className ?? 'text-muted-foreground'}>—</span>;\n return (\n <span className={className} title={profile.email ?? undefined}>\n {profile.displayName}\n </span>\n );\n}\n\n/** Up to two initials from a display name (\"Ada Lovelace\" → \"AL\", \"ada\" → \"A\"). */\nexport function initialsOf(name: string): string {\n return name\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((word) => word[0]!.toUpperCase())\n .join('');\n}\n\nexport interface PersonAvatarProps {\n id: string | null | undefined;\n className?: string;\n}\n\n/**\n * An initials bubble for a user id. Initials come from the RESOLVED name; until the id resolves (or when it never\n * does) the bubble shows a neutral \"?\" rather than letters cut from the UUID, which matched nobody (BOFF-7977,\n * BOFF-8000). The tooltip is the name, plus the email when known.\n */\nexport function PersonAvatar({ id, className }: PersonAvatarProps) {\n const profile = usePersonProfile(id);\n const label = profile\n ? profile.email\n ? `${profile.displayName} (${profile.email})`\n : profile.displayName\n : undefined;\n const initials = profile?.resolved ? initialsOf(profile.displayName) : '';\n return (\n <span className={className} title={label} aria-label={label}>\n {initials || '?'}\n </span>\n );\n}\n"],"mappings":";;;;;AAuDA,IAAM,IAAiD;CACrD,qBAAqB;CACrB,wBAAwB;CACzB,EASY,IAAqB;AAElC,SAAgB,EAAmB,GAAwC;AACzE,QAAO,MAAO;;AAKhB,SAAgB,EAAe,EAAE,gBAAqC;AACpE,QACE,kBAAC,GAAD;EAAO,SAAQ;EAAY,WAAW,KAAa;EAAW,eAAY;YAAkB;EAEpF,CAAA;;AAIZ,IAAM,IAAgB,EAAyC,KAAK;AAEpE,SAAgB,EAAe,EAC7B,gBACA,eAIC;CACD,IAAM,CAAC,GAAK,KAAU,EAA4B,EAAE,CAAC,EAE/C,IAAU,GAAa,MAAe;AAC1C,KAAQ,MAAc,EAAS,SAAS,EAAG,GAAG,IAAW,CAAC,GAAG,GAAU,EAAG,CAAE;IAC3E,EAAE,CAAC,EASA,IAAW,EAPF,QACP,EAAI,KAAK,OAAa;EAAE;EAAS,WAAW;EAAQ,EAAE,EAC5D,CAAC,EAAI,CACN,EAIyC,EAAE,gBAAa,CAAC,EAEpD,IAAQ,SAAe;EAAE;EAAS;EAAU,GAAG,CAAC,GAAS,EAAS,CAAC;AACzE,QAAO,kBAAC,EAAc,UAAf;EAA+B;EAAQ;EAAkC,CAAA;;AAoBlF,SAAgB,EAAiB,GAAoD;CACnF,IAAM,IAAU,EAAW,EAAc,EACnC,IAAU,GAAS,SACnB,IAAiB,IAAK,EAAuB,KAAM,KAAA,GACnD,IAAY,EAAmB,EAAG;AAsBxC,QApBA,QAAgB;AAId,EAAI,KAAM,KAAW,CAAC,KAAkB,CAAC,KAAW,EAAQ,EAAG;IAC9D;EAAC;EAAI;EAAS;EAAgB;EAAU,CAAC,EAEvC,IACD,IACK;EACL,SAAS;EACT,WAAW;EACX,aAAa;EACb,gBAAgB;EAChB,UAAU;EACX,GAIC,IAAkB,OACf,GAAS,SAAS,MAAO,EAAmB;EAAE,SAAS;EAAI,WAAW;EAAQ,CAAC,GAbtE;;AAgBlB,SAAgB,EAAW,EAAE,OAAI,gBAA8B;CAC7D,IAAM,IAAU,EAAiB,EAAG;AAKpC,QAFI,EAAmB,EAAG,GAAS,kBAAC,GAAD,EAA2B,cAAa,CAAA,GACtE,IAEH,kBAAC,QAAD;EAAiB;EAAW,OAAO,EAAQ,SAAS,KAAA;YACjD,EAAQ;EACJ,CAAA,GAJY,kBAAC,QAAD;EAAM,WAAW,KAAa;YAAyB;EAAQ,CAAA;;AAStF,SAAgB,EAAW,GAAsB;AAC/C,QAAO,EACJ,MAAM,CACN,MAAM,MAAM,CACZ,OAAO,QAAQ,CACf,MAAM,GAAG,EAAE,CACX,KAAK,MAAS,EAAK,GAAI,aAAa,CAAC,CACrC,KAAK,GAAG;;AAab,SAAgB,EAAa,EAAE,OAAI,gBAAgC;CACjE,IAAM,IAAU,EAAiB,EAAG,EAC9B,IAAQ,IACV,EAAQ,QACN,GAAG,EAAQ,YAAY,IAAI,EAAQ,MAAM,KACzC,EAAQ,cACV,KAAA;AAEJ,QACE,kBAAC,QAAD;EAAiB;EAAW,OAAO;EAAO,cAAY;aAFvC,GAAS,WAAW,EAAW,EAAQ,YAAY,GAAG,OAGtD;EACR,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare function UnmatchedCrewIdentitiesPanel(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=UnmatchedCrewIdentities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnmatchedCrewIdentities.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/team/UnmatchedCrewIdentities.tsx"],"names":[],"mappings":"AAyKA,wBAAgB,4BAA4B,4CAqD3C"}
@@ -0,0 +1,155 @@
1
+ import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
2
+ import { mutationErrorMessage as t } from "../utils/graphqlErrors.js";
3
+ import { LEAVES_PAGE_CONTEXT as n, throwOnError as r } from "../utils/mutationResult.js";
4
+ import { ErrorLine as i } from "../controls/ConfirmButton.js";
5
+ import { CrewEmployeeSelect as a } from "../controls/Pickers.js";
6
+ import { useState as o } from "react";
7
+ import { AlertTriangle as s } from "lucide-react";
8
+ import { gql as c } from "@apollo/client";
9
+ import { useMutation as l, useQuery as u } from "@apollo/client/react";
10
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
11
+ import { Badge as p, Button as m, Card as h, CardContent as g, CardHeader as _, CardTitle as v } from "@burdenoff/fe-libs/ui";
12
+ //#region src/planmagnet/team/UnmatchedCrewIdentities.tsx
13
+ var y = c`
14
+ query GetUnmatchedCrewIdentities($workspaceId: ID!) {
15
+ planmagnetUnmatchedCrewIdentities(workspaceId: $workspaceId) {
16
+ platformUserId
17
+ displayName
18
+ email
19
+ }
20
+ }
21
+ `, b = c`
22
+ mutation SetMemberIdentityOverride(
23
+ $workspaceId: ID!
24
+ $platformUserId: String!
25
+ $crewEmployeeId: String!
26
+ ) {
27
+ planmagnetSetMemberIdentityOverride(
28
+ workspaceId: $workspaceId
29
+ platformUserId: $platformUserId
30
+ crewEmployeeId: $crewEmployeeId
31
+ ) {
32
+ id
33
+ crewEmployeeId
34
+ }
35
+ }
36
+ `, x = c`
37
+ mutation ClearMemberIdentityOverride($workspaceId: ID!, $platformUserId: String!) {
38
+ planmagnetClearMemberIdentityOverride(
39
+ workspaceId: $workspaceId
40
+ platformUserId: $platformUserId
41
+ )
42
+ }
43
+ `;
44
+ function S({ identity: e, workspaceId: s }) {
45
+ let [c, u] = o(""), [h, g] = o(!1), [_, v] = o(null), [y, S] = l(b, { context: n }), [C, w] = l(x, { context: n }), T = async () => {
46
+ if (c) {
47
+ v(null);
48
+ try {
49
+ await r(y({ variables: {
50
+ workspaceId: s,
51
+ platformUserId: e.platformUserId,
52
+ crewEmployeeId: c
53
+ } })), g(!0);
54
+ } catch (e) {
55
+ v(t(e, "The override could not be saved."));
56
+ }
57
+ }
58
+ }, E = async () => {
59
+ v(null);
60
+ try {
61
+ await r(C({ variables: {
62
+ workspaceId: s,
63
+ platformUserId: e.platformUserId
64
+ } })), g(!1), u("");
65
+ } catch (e) {
66
+ v(t(e, "The override could not be cleared."));
67
+ }
68
+ };
69
+ return /* @__PURE__ */ f("li", {
70
+ className: "flex flex-col gap-2 border rounded-md p-3 sm:flex-row sm:items-center sm:justify-between",
71
+ "data-testid": "unmatched-identity-row",
72
+ children: [/* @__PURE__ */ f("div", {
73
+ className: "min-w-0",
74
+ children: [/* @__PURE__ */ d("p", {
75
+ className: "text-sm font-medium truncate",
76
+ children: e.displayName ?? e.platformUserId
77
+ }), /* @__PURE__ */ d("p", {
78
+ className: "text-xs text-muted-foreground truncate",
79
+ children: e.email ?? "No workspace e-mail on file"
80
+ })]
81
+ }), /* @__PURE__ */ f("div", {
82
+ className: "flex flex-col items-start gap-2 sm:items-end",
83
+ children: [h ? /* @__PURE__ */ f("div", {
84
+ className: "flex items-center gap-2",
85
+ children: [/* @__PURE__ */ d(p, {
86
+ variant: "outline",
87
+ className: "text-xs",
88
+ "data-testid": "unmatched-identity-overridden",
89
+ children: "Override set"
90
+ }), /* @__PURE__ */ d(m, {
91
+ size: "sm",
92
+ variant: "ghost",
93
+ disabled: w.loading,
94
+ "data-testid": "unmatched-identity-clear",
95
+ onClick: () => void E(),
96
+ children: w.loading ? "…" : "Clear override"
97
+ })]
98
+ }) : /* @__PURE__ */ f("div", {
99
+ className: "flex items-center gap-2",
100
+ children: [/* @__PURE__ */ d("div", {
101
+ className: "w-64",
102
+ children: /* @__PURE__ */ d(a, {
103
+ id: `unmatched-${e.platformUserId}`,
104
+ value: c,
105
+ onChange: u,
106
+ noneLabel: "Choose a CrewFoundry employee"
107
+ })
108
+ }), /* @__PURE__ */ d(m, {
109
+ size: "sm",
110
+ disabled: !c || S.loading,
111
+ "data-testid": "unmatched-identity-set",
112
+ onClick: () => void T(),
113
+ children: S.loading ? "Saving…" : "Set override"
114
+ })]
115
+ }), _ && /* @__PURE__ */ d(i, { message: _ })]
116
+ })]
117
+ });
118
+ }
119
+ function C() {
120
+ let { workspaceId: t } = e(), { data: n, loading: r, error: i } = u(y, {
121
+ variables: { workspaceId: t ?? "" },
122
+ skip: !t
123
+ }), a = n?.planmagnetUnmatchedCrewIdentities ?? [];
124
+ return /* @__PURE__ */ f(h, {
125
+ id: "unmatched-crew-identities",
126
+ "data-testid": "unmatched-crew-identities-panel",
127
+ children: [/* @__PURE__ */ f(_, { children: [/* @__PURE__ */ f(v, {
128
+ className: "flex items-center gap-2",
129
+ children: [/* @__PURE__ */ d(s, { className: "h-4 w-4 text-status-warning-text" }), "Unmatched CrewFoundry identities"]
130
+ }), /* @__PURE__ */ d("p", {
131
+ className: "text-xs text-muted-foreground",
132
+ children: "An override pins one workspace member directly to one CrewFoundry employee record, for when the default best-effort e-mail match can't find — or finds the wrong — employee for them."
133
+ })] }), /* @__PURE__ */ d(g, { children: r && !n ? /* @__PURE__ */ d("p", {
134
+ className: "text-sm text-muted-foreground",
135
+ children: "Loading…"
136
+ }) : i ? /* @__PURE__ */ f("p", {
137
+ className: "text-sm text-status-error-text",
138
+ children: ["Unmatched identities could not be loaded: ", i.message]
139
+ }) : a.length === 0 ? /* @__PURE__ */ d("p", {
140
+ className: "text-sm text-muted-foreground py-2",
141
+ "data-testid": "unmatched-identities-empty",
142
+ children: "Every assigned member currently matches a CrewFoundry employee — nothing to fix."
143
+ }) : /* @__PURE__ */ d("ul", {
144
+ className: "space-y-2",
145
+ children: a.map((e) => /* @__PURE__ */ d(S, {
146
+ identity: e,
147
+ workspaceId: t ?? ""
148
+ }, e.platformUserId))
149
+ }) })]
150
+ });
151
+ }
152
+ //#endregion
153
+ export { C as UnmatchedCrewIdentitiesPanel };
154
+
155
+ //# sourceMappingURL=UnmatchedCrewIdentities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnmatchedCrewIdentities.js","names":[],"sources":["../../../src/planmagnet/team/UnmatchedCrewIdentities.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Badge, Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { AlertTriangle } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { CrewEmployeeSelect } from '../controls/Pickers';\n\n/**\n * CWF-30 admin remediation panel, on the workspace Members page (`/team/team-page`):\n * `planmagnetUnmatchedCrewIdentities` lists workspace members with a live work-item assignment whose identity\n * could not be resolved to a CrewFoundry employee by the default best-effort e-mail match (see\n * `planmagnetSprintCapacity`). Setting an override here pins one member directly to one CrewFoundry employee\n * record so sprint capacity can find them; clearing it returns that member to the e-mail match.\n *\n * Setting/clearing an override is deliberately published with `LEAVES_PAGE_CONTEXT` (skips fe-libs' global\n * post-mutation auto-refresh): the row that was just fixed would otherwise vanish from this list on its own\n * next refetch mid-interaction, taking the \"Clear override\" affordance with it before the admin could use it.\n * The row's own local state (not a refetch) is what shows the result of the write it just made; the row drops\n * off this list for real only the next time the page itself reloads and finds the member matched.\n */\n\nconst GET_UNMATCHED = gql`\n query GetUnmatchedCrewIdentities($workspaceId: ID!) {\n planmagnetUnmatchedCrewIdentities(workspaceId: $workspaceId) {\n platformUserId\n displayName\n email\n }\n }\n`;\n\nconst SET_OVERRIDE = gql`\n mutation SetMemberIdentityOverride(\n $workspaceId: ID!\n $platformUserId: String!\n $crewEmployeeId: String!\n ) {\n planmagnetSetMemberIdentityOverride(\n workspaceId: $workspaceId\n platformUserId: $platformUserId\n crewEmployeeId: $crewEmployeeId\n ) {\n id\n crewEmployeeId\n }\n }\n`;\n\nconst CLEAR_OVERRIDE = gql`\n mutation ClearMemberIdentityOverride($workspaceId: ID!, $platformUserId: String!) {\n planmagnetClearMemberIdentityOverride(\n workspaceId: $workspaceId\n platformUserId: $platformUserId\n )\n }\n`;\n\ninterface UnmatchedIdentity {\n platformUserId: string;\n displayName: string | null;\n email: string | null;\n}\n\nfunction UnmatchedIdentityRow({\n identity,\n workspaceId,\n}: {\n identity: UnmatchedIdentity;\n workspaceId: string;\n}) {\n const [crewEmployeeId, setCrewEmployeeId] = useState('');\n const [overridden, setOverridden] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [setOverride, setState] = useMutation(SET_OVERRIDE, { context: LEAVES_PAGE_CONTEXT });\n const [clearOverride, clearState] = useMutation(CLEAR_OVERRIDE, {\n context: LEAVES_PAGE_CONTEXT,\n });\n\n const onSet = async () => {\n if (!crewEmployeeId) return;\n setError(null);\n try {\n await throwOnError(\n setOverride({\n variables: { workspaceId, platformUserId: identity.platformUserId, crewEmployeeId },\n })\n );\n setOverridden(true);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The override could not be saved.'));\n }\n };\n\n const onClear = async () => {\n setError(null);\n try {\n await throwOnError(\n clearOverride({ variables: { workspaceId, platformUserId: identity.platformUserId } })\n );\n setOverridden(false);\n setCrewEmployeeId('');\n } catch (e) {\n setError(mutationErrorMessage(e, 'The override could not be cleared.'));\n }\n };\n\n return (\n <li\n className=\"flex flex-col gap-2 border rounded-md p-3 sm:flex-row sm:items-center sm:justify-between\"\n data-testid=\"unmatched-identity-row\"\n >\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium truncate\">\n {identity.displayName ?? identity.platformUserId}\n </p>\n <p className=\"text-xs text-muted-foreground truncate\">\n {identity.email ?? 'No workspace e-mail on file'}\n </p>\n </div>\n <div className=\"flex flex-col items-start gap-2 sm:items-end\">\n {overridden ? (\n <div className=\"flex items-center gap-2\">\n <Badge\n variant=\"outline\"\n className=\"text-xs\"\n data-testid=\"unmatched-identity-overridden\"\n >\n Override set\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={clearState.loading}\n data-testid=\"unmatched-identity-clear\"\n onClick={() => void onClear()}\n >\n {clearState.loading ? '…' : 'Clear override'}\n </Button>\n </div>\n ) : (\n <div className=\"flex items-center gap-2\">\n <div className=\"w-64\">\n <CrewEmployeeSelect\n id={`unmatched-${identity.platformUserId}`}\n value={crewEmployeeId}\n onChange={setCrewEmployeeId}\n noneLabel=\"Choose a CrewFoundry employee\"\n />\n </div>\n <Button\n size=\"sm\"\n disabled={!crewEmployeeId || setState.loading}\n data-testid=\"unmatched-identity-set\"\n onClick={() => void onSet()}\n >\n {setState.loading ? 'Saving…' : 'Set override'}\n </Button>\n </div>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n </li>\n );\n}\n\nexport function UnmatchedCrewIdentitiesPanel() {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetUnmatchedCrewIdentities: UnmatchedIdentity[];\n }>(GET_UNMATCHED, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n const identities = data?.planmagnetUnmatchedCrewIdentities ?? [];\n\n return (\n <Card id=\"unmatched-crew-identities\" data-testid=\"unmatched-crew-identities-panel\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <AlertTriangle className=\"h-4 w-4 text-status-warning-text\" />\n Unmatched CrewFoundry identities\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n An override pins one workspace member directly to one CrewFoundry employee record, for\n when the default best-effort e-mail match can&apos;t find — or finds the wrong — employee\n for them.\n </p>\n </CardHeader>\n <CardContent>\n {/* Only before the first load: a refetch must not blank the panel (it would unmount every\n row's own in-progress edit/local override state). */}\n {loading && !data ? (\n <p className=\"text-sm text-muted-foreground\">Loading…</p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n Unmatched identities could not be loaded: {error.message}\n </p>\n ) : identities.length === 0 ? (\n <p\n className=\"text-sm text-muted-foreground py-2\"\n data-testid=\"unmatched-identities-empty\"\n >\n Every assigned member currently matches a CrewFoundry employee — nothing to fix.\n </p>\n ) : (\n <ul className=\"space-y-2\">\n {identities.map((identity) => (\n <UnmatchedIdentityRow\n key={identity.platformUserId}\n identity={identity}\n workspaceId={workspaceId ?? ''}\n />\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAyBA,IAAM,IAAgB,CAAG;;;;;;;;GAUnB,IAAe,CAAG;;;;;;;;;;;;;;;GAiBlB,IAAiB,CAAG;;;;;;;;AAe1B,SAAS,EAAqB,EAC5B,aACA,kBAIC;CACD,IAAM,CAAC,GAAgB,KAAqB,EAAS,GAAG,EAClD,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAa,KAAY,EAAY,GAAc,EAAE,SAAS,GAAqB,CAAC,EACrF,CAAC,GAAe,KAAc,EAAY,GAAgB,EAC9D,SAAS,GACV,CAAC,EAEI,IAAQ,YAAY;AACnB,SACL;KAAS,KAAK;AACd,OAAI;AAMF,IALA,MAAM,EACJ,EAAY,EACV,WAAW;KAAE;KAAa,gBAAgB,EAAS;KAAgB;KAAgB,EACpF,CAAC,CACH,EACD,EAAc,GAAK;YACZ,GAAG;AACV,MAAS,EAAqB,GAAG,mCAAmC,CAAC;;;IAInE,IAAU,YAAY;AAC1B,IAAS,KAAK;AACd,MAAI;AAKF,GAJA,MAAM,EACJ,EAAc,EAAE,WAAW;IAAE;IAAa,gBAAgB,EAAS;IAAgB,EAAE,CAAC,CACvF,EACD,EAAc,GAAM,EACpB,EAAkB,GAAG;WACd,GAAG;AACV,KAAS,EAAqB,GAAG,qCAAqC,CAAC;;;AAI3E,QACE,kBAAC,MAAD;EACE,WAAU;EACV,eAAY;YAFd,CAIE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cACV,EAAS,eAAe,EAAS;IAChC,CAAA,EACJ,kBAAC,KAAD;IAAG,WAAU;cACV,EAAS,SAAS;IACjB,CAAA,CACA;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,SAAQ;KACR,WAAU;KACV,eAAY;eACb;KAEO,CAAA,EACR,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,UAAU,EAAW;KACrB,eAAY;KACZ,eAAe,KAAK,GAAS;eAE5B,EAAW,UAAU,MAAM;KACrB,CAAA,CACL;QAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,IAAI,aAAa,EAAS;MAC1B,OAAO;MACP,UAAU;MACV,WAAU;MACV,CAAA;KACE,CAAA,EACN,kBAAC,GAAD;KACE,MAAK;KACL,UAAU,CAAC,KAAkB,EAAS;KACtC,eAAY;KACZ,eAAe,KAAK,GAAO;eAE1B,EAAS,UAAU,YAAY;KACzB,CAAA,CACL;OAEP,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC;KACH;;;AAIT,SAAgB,IAA+B;CAC7C,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAe;EAChB,WAAW,EAAE,aAAa,KAAe,IAAI;EAC7C,MAAM,CAAC;EACR,CAAC,EACI,IAAa,GAAM,qCAAqC,EAAE;AAEhE,QACE,kBAAC,GAAD;EAAM,IAAG;EAA4B,eAAY;YAAjD,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;GAAW,WAAU;aAArB,CACE,kBAAC,GAAD,EAAe,WAAU,oCAAqC,CAAA,EAAA,mCAEpD;MACZ,kBAAC,KAAD;GAAG,WAAU;aAAgC;GAIzC,CAAA,CACO,EAAA,CAAA,EACb,kBAAC,GAAD,EAAA,UAGG,KAAW,CAAC,IACX,kBAAC,KAAD;GAAG,WAAU;aAAgC;GAAY,CAAA,GACvD,IACF,kBAAC,KAAD;GAAG,WAAU;aAAb,CAA8C,8CACD,EAAM,QAC/C;OACF,EAAW,WAAW,IACxB,kBAAC,KAAD;GACE,WAAU;GACV,eAAY;aACb;GAEG,CAAA,GAEJ,kBAAC,MAAD;GAAI,WAAU;aACX,EAAW,KAAK,MACf,kBAAC,GAAD;IAEY;IACV,aAAa,KAAe;IAC5B,EAHK,EAAS,eAGd,CACF;GACC,CAAA,EAEK,CAAA,CACT"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Outbound webhook subscriptions (AUT-33) — shared shapes and constants. GraphQL Hive doc comments
3
+ * (`graphql/wspace.graphql`, `PlanmagnetWebhookSubscription*`) are the source of truth for all of this, not
4
+ * guesswork:
5
+ *
6
+ * - `events` is a list of `PlanmagnetAutomationTriggerType` values (the SAME enum the automation rule builder's
7
+ * trigger select uses — see `automation/automationRuleTypes.ts`). Every value delivers a real webhook today
8
+ * EXCEPT `WEBHOOK` itself, which is accepted and saveable (so a subscription can be prepared ahead of time)
9
+ * but not yet live — gated on the public-gateway route for inbound GitHub deliveries (Platform ticket
10
+ * BOFF-8238). `SCHEDULE` and `MILESTONE_DUE` deliver from `automation-scheduler.ts`'s periodic job rather than
11
+ * a synchronous write, same as they do for automation rules.
12
+ * - The signing secret (`PlanmagnetWebhookSubscriptionWithSecret.secret`) is returned in PLAINTEXT exactly once,
13
+ * on create and again on rotate — it cannot be read back from the API afterwards. See
14
+ * `controls/WebhookSubscriptionControls.tsx` for the "shown once, every close path resets it" discipline this
15
+ * demands (the same one `reports/ShareDialog.tsx` was fixed to use, #45).
16
+ * - `consecutiveFailures` resets to 0 on the next successful delivery, or whenever an update moves `status` back
17
+ * to ACTIVE; reaching the engine's own failure threshold moves `status` to DISABLED automatically.
18
+ */
19
+ export declare const WEBHOOK_SUBSCRIPTION_STATUSES: readonly ["ACTIVE", "PAUSED", "DISABLED"];
20
+ export type WebhookSubscriptionStatus = (typeof WEBHOOK_SUBSCRIPTION_STATUSES)[number];
21
+ export declare const WEBHOOK_DELIVERY_STATUSES: readonly ["SUCCESS", "FAILED"];
22
+ export type WebhookDeliveryStatus = (typeof WEBHOOK_DELIVERY_STATUSES)[number];
23
+ export interface WebhookSubscriptionRow {
24
+ id: string;
25
+ url: string;
26
+ events: string[];
27
+ status: WebhookSubscriptionStatus;
28
+ consecutiveFailures: number;
29
+ lastDeliveryAt: string | null;
30
+ lastDeliveryStatus: WebhookDeliveryStatus | null;
31
+ createdAt: string;
32
+ createdBy: string;
33
+ updatedAt: string;
34
+ updatedBy: string | null;
35
+ disabledAt: string | null;
36
+ }
37
+ export interface WebhookSubscriptionDeliveryRow {
38
+ id: string;
39
+ event: string;
40
+ status: WebhookDeliveryStatus;
41
+ /** Null for a connection-level failure (DNS/SSRF refusal, timeout, connection reset) that never got far
42
+ * enough to receive one. */
43
+ responseStatus: number | null;
44
+ attempts: number;
45
+ error: string | null;
46
+ deliveredAt: string;
47
+ }
48
+ /** `PlanmagnetCreateWebhookSubscriptionInput.url` must be an absolute `https://` URL — the server's own SSRF
49
+ * guard (private/loopback/link-local/metadata ranges) is the real gate and runs again at every delivery; this
50
+ * is only the client-side sanity check that catches an obviously wrong URL before a round trip. */
51
+ export declare function isPlausibleWebhookUrl(value: string): boolean;
52
+ /** The event-select option suffix — `WEBHOOK` is accepted and saveable but not yet live (BOFF-8238). */
53
+ export declare function webhookEventOptionSuffix(trigger: string): string;
54
+ //# sourceMappingURL=webhookTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhookTypes.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/webhooks/webhookTypes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,6BAA6B,2CAA4C,CAAC;AACvF,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvF,eAAO,MAAM,yBAAyB,gCAAiC,CAAC;AACxE,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/E,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,yBAAyB,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kBAAkB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,qBAAqB,CAAC;IAC9B;iCAC6B;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;oGAEoG;AACpG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAQ5D;AAED,wGAAwG;AACxG,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhE"}
@@ -0,0 +1,17 @@
1
+ //#region src/planmagnet/webhooks/webhookTypes.ts
2
+ function e(e) {
3
+ if (!/^https:\/\//i.test(e.trim())) return !1;
4
+ try {
5
+ let t = new URL(e.trim());
6
+ return t.protocol === "https:" && t.hostname.length > 0;
7
+ } catch {
8
+ return !1;
9
+ }
10
+ }
11
+ function t(e) {
12
+ return e === "WEBHOOK" ? " (not live yet)" : "";
13
+ }
14
+ //#endregion
15
+ export { e as isPlausibleWebhookUrl, t as webhookEventOptionSuffix };
16
+
17
+ //# sourceMappingURL=webhookTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhookTypes.js","names":[],"sources":["../../../src/planmagnet/webhooks/webhookTypes.ts"],"sourcesContent":["/**\n * Outbound webhook subscriptions (AUT-33) — shared shapes and constants. GraphQL Hive doc comments\n * (`graphql/wspace.graphql`, `PlanmagnetWebhookSubscription*`) are the source of truth for all of this, not\n * guesswork:\n *\n * - `events` is a list of `PlanmagnetAutomationTriggerType` values (the SAME enum the automation rule builder's\n * trigger select uses — see `automation/automationRuleTypes.ts`). Every value delivers a real webhook today\n * EXCEPT `WEBHOOK` itself, which is accepted and saveable (so a subscription can be prepared ahead of time)\n * but not yet live — gated on the public-gateway route for inbound GitHub deliveries (Platform ticket\n * BOFF-8238). `SCHEDULE` and `MILESTONE_DUE` deliver from `automation-scheduler.ts`'s periodic job rather than\n * a synchronous write, same as they do for automation rules.\n * - The signing secret (`PlanmagnetWebhookSubscriptionWithSecret.secret`) is returned in PLAINTEXT exactly once,\n * on create and again on rotate — it cannot be read back from the API afterwards. See\n * `controls/WebhookSubscriptionControls.tsx` for the \"shown once, every close path resets it\" discipline this\n * demands (the same one `reports/ShareDialog.tsx` was fixed to use, #45).\n * - `consecutiveFailures` resets to 0 on the next successful delivery, or whenever an update moves `status` back\n * to ACTIVE; reaching the engine's own failure threshold moves `status` to DISABLED automatically.\n */\n\nexport const WEBHOOK_SUBSCRIPTION_STATUSES = ['ACTIVE', 'PAUSED', 'DISABLED'] as const;\nexport type WebhookSubscriptionStatus = (typeof WEBHOOK_SUBSCRIPTION_STATUSES)[number];\n\nexport const WEBHOOK_DELIVERY_STATUSES = ['SUCCESS', 'FAILED'] as const;\nexport type WebhookDeliveryStatus = (typeof WEBHOOK_DELIVERY_STATUSES)[number];\n\nexport interface WebhookSubscriptionRow {\n id: string;\n url: string;\n events: string[];\n status: WebhookSubscriptionStatus;\n consecutiveFailures: number;\n lastDeliveryAt: string | null;\n lastDeliveryStatus: WebhookDeliveryStatus | null;\n createdAt: string;\n createdBy: string;\n updatedAt: string;\n updatedBy: string | null;\n disabledAt: string | null;\n}\n\nexport interface WebhookSubscriptionDeliveryRow {\n id: string;\n event: string;\n status: WebhookDeliveryStatus;\n /** Null for a connection-level failure (DNS/SSRF refusal, timeout, connection reset) that never got far\n * enough to receive one. */\n responseStatus: number | null;\n attempts: number;\n error: string | null;\n deliveredAt: string;\n}\n\n/** `PlanmagnetCreateWebhookSubscriptionInput.url` must be an absolute `https://` URL — the server's own SSRF\n * guard (private/loopback/link-local/metadata ranges) is the real gate and runs again at every delivery; this\n * is only the client-side sanity check that catches an obviously wrong URL before a round trip. */\nexport function isPlausibleWebhookUrl(value: string): boolean {\n if (!/^https:\\/\\//i.test(value.trim())) return false;\n try {\n const url = new URL(value.trim());\n return url.protocol === 'https:' && url.hostname.length > 0;\n } catch {\n return false;\n }\n}\n\n/** The event-select option suffix — `WEBHOOK` is accepted and saveable but not yet live (BOFF-8238). */\nexport function webhookEventOptionSuffix(trigger: string): string {\n return trigger === 'WEBHOOK' ? ' (not live yet)' : '';\n}\n"],"mappings":";AAuDA,SAAgB,EAAsB,GAAwB;AAC5D,KAAI,CAAC,eAAe,KAAK,EAAM,MAAM,CAAC,CAAE,QAAO;AAC/C,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,EAAM,MAAM,CAAC;AACjC,SAAO,EAAI,aAAa,YAAY,EAAI,SAAS,SAAS;SACpD;AACN,SAAO;;;AAKX,SAAgB,EAAyB,GAAyB;AAChE,QAAO,MAAY,YAAY,oBAAoB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.1001.5",
3
+ "version": "2026.1001.7",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [