paperclip-github-plugin 0.5.3 → 0.6.1

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.
package/dist/ui/index.js CHANGED
@@ -22995,6 +22995,28 @@ var EMPTY_SETTINGS = {
22995
22995
  advancedSettings: DEFAULT_ADVANCED_SETTINGS,
22996
22996
  availableAssignees: []
22997
22997
  };
22998
+ var EMPTY_DASHBOARD_METRICS = {
22999
+ status: "company_required",
23000
+ historyWindowDays: 14,
23001
+ comparisonWindowDays: 30,
23002
+ backlog: {
23003
+ history: []
23004
+ },
23005
+ githubIssuesClosed: {
23006
+ currentPeriodCount: 0,
23007
+ previousPeriodCount: 0,
23008
+ history: []
23009
+ },
23010
+ paperclipPullRequestsCreated: {
23011
+ currentPeriodCount: 0,
23012
+ previousPeriodCount: 0,
23013
+ history: []
23014
+ },
23015
+ notes: {
23016
+ backlogHistoryAvailable: false,
23017
+ activityHistoryAvailable: false
23018
+ }
23019
+ };
22998
23020
  function createIdleSyncState() {
22999
23021
  return {
23000
23022
  status: "idle"
@@ -26142,7 +26164,7 @@ var WIDGET_STYLES = `
26142
26164
  .ghsync-widget__stats {
26143
26165
  display: grid;
26144
26166
  gap: 12px;
26145
- grid-template-columns: repeat(2, minmax(0, 1fr));
26167
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
26146
26168
  border-top: 1px solid var(--ghsync-border-soft);
26147
26169
  padding-top: 14px;
26148
26170
  }
@@ -26155,39 +26177,117 @@ var WIDGET_STYLES = `
26155
26177
 
26156
26178
  .ghsync-widget__stat {
26157
26179
  display: grid;
26158
- gap: 6px;
26159
- padding: 12px;
26180
+ gap: 10px;
26181
+ padding: 14px;
26160
26182
  border: 1px solid var(--ghsync-border-soft);
26161
26183
  border-radius: 10px;
26162
26184
  background: var(--ghsync-surfaceAlt);
26163
26185
  }
26164
26186
 
26165
- .ghsync-widget__stat--emphasized {
26166
- border-color: var(--ghsync-danger-border);
26167
- background: var(--ghsync-dangerBg);
26187
+ .ghsync-widget__stat--success {
26188
+ border-color: var(--ghsync-success-border);
26189
+ }
26190
+
26191
+ .ghsync-widget__stat--warning {
26192
+ border-color: var(--ghsync-warning-border);
26168
26193
  }
26169
26194
 
26170
- .ghsync-widget__stat span {
26195
+ .ghsync-widget__stat--info {
26196
+ border-color: var(--ghsync-info-border);
26197
+ }
26198
+
26199
+ .ghsync-widget__stat-top {
26200
+ display: flex;
26201
+ align-items: flex-start;
26202
+ justify-content: space-between;
26203
+ gap: 12px;
26204
+ }
26205
+
26206
+ .ghsync-widget__stat-value {
26207
+ display: grid;
26208
+ gap: 6px;
26209
+ }
26210
+
26211
+ .ghsync-widget__stat-label {
26171
26212
  display: block;
26172
26213
  font-size: 12px;
26173
26214
  font-weight: 600;
26174
26215
  color: var(--ghsync-title);
26175
26216
  }
26176
26217
 
26177
- .ghsync-widget__stat strong {
26218
+ .ghsync-widget__stat-value strong {
26178
26219
  display: block;
26179
26220
  font-size: 24px;
26180
26221
  line-height: 1;
26181
26222
  color: var(--ghsync-title);
26182
26223
  }
26183
26224
 
26184
- .ghsync-widget__stat p {
26225
+ .ghsync-widget__stat-change {
26226
+ margin: 0;
26227
+ font-size: 11px;
26228
+ line-height: 1.45;
26229
+ color: var(--ghsync-muted);
26230
+ }
26231
+
26232
+ .ghsync-widget__stat-change--success {
26233
+ color: var(--ghsync-success-text);
26234
+ }
26235
+
26236
+ .ghsync-widget__stat-change--warning {
26237
+ color: var(--ghsync-warning-text);
26238
+ }
26239
+
26240
+ .ghsync-widget__stat-change--info {
26241
+ color: var(--ghsync-info-text);
26242
+ }
26243
+
26244
+ .ghsync-widget__stat-note {
26185
26245
  margin: 0;
26186
26246
  color: var(--ghsync-muted);
26187
26247
  font-size: 11px;
26188
26248
  line-height: 1.5;
26189
26249
  }
26190
26250
 
26251
+ .ghsync-widget__trend {
26252
+ color: var(--ghsync-muted);
26253
+ }
26254
+
26255
+ .ghsync-widget__trend svg {
26256
+ display: block;
26257
+ width: 100%;
26258
+ height: 32px;
26259
+ }
26260
+
26261
+ .ghsync-widget__trend--success {
26262
+ color: var(--ghsync-success-text);
26263
+ }
26264
+
26265
+ .ghsync-widget__trend--warning {
26266
+ color: var(--ghsync-warning-text);
26267
+ }
26268
+
26269
+ .ghsync-widget__trend--info {
26270
+ color: var(--ghsync-info-text);
26271
+ }
26272
+
26273
+ .ghsync-widget__trend-line {
26274
+ fill: none;
26275
+ stroke: currentColor;
26276
+ stroke-width: 2;
26277
+ stroke-linecap: round;
26278
+ stroke-linejoin: round;
26279
+ }
26280
+
26281
+ .ghsync-widget__trend-area {
26282
+ fill: currentColor;
26283
+ opacity: 0.12;
26284
+ }
26285
+
26286
+ .ghsync-widget__trend-bar {
26287
+ fill: currentColor;
26288
+ opacity: 0.9;
26289
+ }
26290
+
26191
26291
  .ghsync-widget__summary {
26192
26292
  display: grid;
26193
26293
  gap: 4px;
@@ -26430,7 +26530,6 @@ var WIDGET_STYLES = `
26430
26530
  @media (max-width: 720px) {
26431
26531
  .ghsync-widget__stats {
26432
26532
  grid-template-columns: minmax(0, 1fr);
26433
- gap: 12px;
26434
26533
  }
26435
26534
 
26436
26535
  .ghsync-widget__top,
@@ -26439,11 +26538,6 @@ var WIDGET_STYLES = `
26439
26538
  align-items: stretch;
26440
26539
  }
26441
26540
 
26442
- .ghsync-widget__stat {
26443
- padding-left: 0;
26444
- border-left: 0;
26445
- }
26446
-
26447
26541
  .ghsync-widget__button-row {
26448
26542
  width: 100%;
26449
26543
  }
@@ -28681,6 +28775,68 @@ function getSyncMetricCards(params) {
28681
28775
  }
28682
28776
  ];
28683
28777
  }
28778
+ function getSyncMetricCardTone(card) {
28779
+ if (card.key === "errored") {
28780
+ return card.emphasized ? "warning" : "success";
28781
+ }
28782
+ return card.value > 0 ? "success" : "info";
28783
+ }
28784
+ function getKpiDashboardSummary(params) {
28785
+ if (!params.hasCompanyContext) {
28786
+ return {
28787
+ label: "Company required",
28788
+ tone: "warning",
28789
+ title: "Open a company dashboard",
28790
+ body: "Compare backlog, issue closure, and Paperclip PR creation over time."
28791
+ };
28792
+ }
28793
+ if (params.syncIssue === "missing_token") {
28794
+ return {
28795
+ label: "Setup required",
28796
+ tone: "warning",
28797
+ title: "Finish setup",
28798
+ body: "Save a GitHub token to start KPI tracking."
28799
+ };
28800
+ }
28801
+ if (params.syncIssue === "missing_board_access") {
28802
+ return {
28803
+ label: "Board access required",
28804
+ tone: "warning",
28805
+ title: "Connect Paperclip board access",
28806
+ body: "This deployment needs board access before KPI history can refresh."
28807
+ };
28808
+ }
28809
+ if (params.metrics.status === "no_mappings") {
28810
+ return {
28811
+ label: "Setup required",
28812
+ tone: "warning",
28813
+ title: "Map a repository",
28814
+ body: "Add at least one repository, then run a full sync."
28815
+ };
28816
+ }
28817
+ if (params.syncState.status === "running") {
28818
+ return {
28819
+ label: "Syncing",
28820
+ tone: "info",
28821
+ title: "KPI history is updating",
28822
+ body: "Backlog and activity history refresh during sync."
28823
+ };
28824
+ }
28825
+ if (!params.metrics.notes.backlogHistoryAvailable && !params.metrics.notes.activityHistoryAvailable) {
28826
+ return {
28827
+ label: "Waiting on first sync",
28828
+ tone: "info",
28829
+ title: "Run the first full sync",
28830
+ body: "The first sync seeds backlog and issue history."
28831
+ };
28832
+ }
28833
+ return {
28834
+ label: "Ready",
28835
+ tone: params.syncState.status === "success" ? "success" : "info",
28836
+ title: "Company delivery KPIs",
28837
+ body: "Track backlog, issue closure, and Paperclip PR creation against recent history."
28838
+ };
28839
+ }
28684
28840
  function getDashboardSummary(params) {
28685
28841
  const cadence = formatScheduleFrequency(params.scheduleFrequencyMinutes);
28686
28842
  const activeRateLimitPause = getActiveRateLimitPause(params.syncState);
@@ -28756,6 +28912,157 @@ function getDashboardSummary(params) {
28756
28912
  body: `Your repository mapping is in place. Automatic sync runs ${cadence}.`
28757
28913
  };
28758
28914
  }
28915
+ function formatWidgetMetricValue(value) {
28916
+ return typeof value === "number" ? String(value) : "\u2014";
28917
+ }
28918
+ function getPeriodDeltaTone(current, previous3) {
28919
+ if (current > previous3) {
28920
+ return "success";
28921
+ }
28922
+ if (current < previous3) {
28923
+ return "warning";
28924
+ }
28925
+ return "neutral";
28926
+ }
28927
+ function getBacklogDeltaTone(current, previous3) {
28928
+ if (current < previous3) {
28929
+ return "success";
28930
+ }
28931
+ if (current > previous3) {
28932
+ return "warning";
28933
+ }
28934
+ return "neutral";
28935
+ }
28936
+ function describePeriodChange(current, previous3, comparisonWindowDays) {
28937
+ if (current > previous3) {
28938
+ return `+${current - previous3} vs previous ${comparisonWindowDays} days`;
28939
+ }
28940
+ if (current < previous3) {
28941
+ return `-${previous3 - current} vs previous ${comparisonWindowDays} days`;
28942
+ }
28943
+ return `No change vs previous ${comparisonWindowDays} days`;
28944
+ }
28945
+ function describeBacklogChange(current, previous3, comparisonWindowDays) {
28946
+ if (current === void 0 || previous3 === void 0) {
28947
+ return "Need more sync history to compare backlog.";
28948
+ }
28949
+ if (current < previous3) {
28950
+ return `-${previous3 - current} open issues vs ${comparisonWindowDays} days ago`;
28951
+ }
28952
+ if (current > previous3) {
28953
+ return `+${current - previous3} open issues vs ${comparisonWindowDays} days ago`;
28954
+ }
28955
+ return `No change vs ${comparisonWindowDays} days ago`;
28956
+ }
28957
+ function buildDashboardKpiCards(params) {
28958
+ const { metrics, hasCompanyContext } = params;
28959
+ const genericContextNote = !hasCompanyContext ? "Open in a company dashboard." : metrics.status === "no_mappings" ? "Add a repository mapping." : null;
28960
+ const backlogAvailable = hasCompanyContext && metrics.status === "ready" && metrics.notes.backlogHistoryAvailable && typeof metrics.backlog.currentOpenIssueCount === "number";
28961
+ const backlogCurrent = metrics.backlog.currentOpenIssueCount;
28962
+ const backlogComparison = metrics.backlog.comparisonOpenIssueCount;
28963
+ const closedIssuesAvailable = hasCompanyContext && metrics.status === "ready" && metrics.notes.activityHistoryAvailable;
28964
+ const createdAvailable = closedIssuesAvailable;
28965
+ return [
28966
+ {
28967
+ key: "backlog",
28968
+ title: "Open GitHub backlog",
28969
+ valueLabel: formatWidgetMetricValue(backlogCurrent),
28970
+ changeLabel: describeBacklogChange(backlogCurrent, backlogComparison, metrics.comparisonWindowDays),
28971
+ note: genericContextNote ?? (backlogAvailable ? metrics.backlog.lastCapturedAt ? `Snapshot ${formatDate(metrics.backlog.lastCapturedAt, metrics.backlog.lastCapturedAt)}.` : "Latest sync snapshot." : "Run a full sync to seed backlog history."),
28972
+ tone: backlogAvailable && backlogComparison !== void 0 && backlogCurrent !== void 0 ? getBacklogDeltaTone(backlogCurrent, backlogComparison) : "neutral",
28973
+ chartKind: "line",
28974
+ history: metrics.backlog.history.map((point5) => point5.value),
28975
+ available: backlogAvailable
28976
+ },
28977
+ {
28978
+ key: "closed-issues",
28979
+ title: "GitHub issues closed",
28980
+ valueLabel: String(metrics.githubIssuesClosed.currentPeriodCount),
28981
+ changeLabel: describePeriodChange(
28982
+ metrics.githubIssuesClosed.currentPeriodCount,
28983
+ metrics.githubIssuesClosed.previousPeriodCount,
28984
+ metrics.comparisonWindowDays
28985
+ ),
28986
+ note: genericContextNote ?? (closedIssuesAvailable ? metrics.githubIssuesClosed.lastRecordedAt ? `Through ${formatDate(metrics.githubIssuesClosed.lastRecordedAt, metrics.githubIssuesClosed.lastRecordedAt)}.` : "From sync-detected closures." : "Appears after sync records closures."),
28987
+ tone: getPeriodDeltaTone(
28988
+ metrics.githubIssuesClosed.currentPeriodCount,
28989
+ metrics.githubIssuesClosed.previousPeriodCount
28990
+ ),
28991
+ chartKind: "bars",
28992
+ history: metrics.githubIssuesClosed.history.map((point5) => point5.value),
28993
+ available: closedIssuesAvailable
28994
+ },
28995
+ {
28996
+ key: "created-prs",
28997
+ title: "Paperclip PRs created",
28998
+ valueLabel: String(metrics.paperclipPullRequestsCreated.currentPeriodCount),
28999
+ changeLabel: describePeriodChange(
29000
+ metrics.paperclipPullRequestsCreated.currentPeriodCount,
29001
+ metrics.paperclipPullRequestsCreated.previousPeriodCount,
29002
+ metrics.comparisonWindowDays
29003
+ ),
29004
+ note: genericContextNote ?? (createdAvailable ? metrics.paperclipPullRequestsCreated.lastRecordedAt ? `Through ${formatDate(metrics.paperclipPullRequestsCreated.lastRecordedAt, metrics.paperclipPullRequestsCreated.lastRecordedAt)}.` : "From Paperclip-attributed PR events." : "Appears after Paperclip records PR creation."),
29005
+ tone: getPeriodDeltaTone(
29006
+ metrics.paperclipPullRequestsCreated.currentPeriodCount,
29007
+ metrics.paperclipPullRequestsCreated.previousPeriodCount
29008
+ ),
29009
+ chartKind: "bars",
29010
+ history: metrics.paperclipPullRequestsCreated.history.map((point5) => point5.value),
29011
+ available: createdAvailable
29012
+ }
29013
+ ];
29014
+ }
29015
+ function buildLineChartPath(values, width, height) {
29016
+ if (values.length === 0) {
29017
+ return "";
29018
+ }
29019
+ const min = Math.min(...values);
29020
+ const max = Math.max(...values);
29021
+ const range = max - min || 1;
29022
+ return values.map((value, index2) => {
29023
+ const x = values.length === 1 ? width / 2 : index2 / (values.length - 1) * width;
29024
+ const y = height - (value - min) / range * (height - 6) - 3;
29025
+ return `${index2 === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`;
29026
+ }).join(" ");
29027
+ }
29028
+ function buildLineChartArea(values, width, height) {
29029
+ const linePath = buildLineChartPath(values, width, height);
29030
+ if (!linePath || values.length === 0) {
29031
+ return "";
29032
+ }
29033
+ const firstX = values.length === 1 ? width / 2 : 0;
29034
+ const lastX = values.length === 1 ? width / 2 : width;
29035
+ return `${linePath} L ${lastX.toFixed(2)} ${height} L ${firstX.toFixed(2)} ${height} Z`;
29036
+ }
29037
+ function DashboardTrendGraphic(props) {
29038
+ const values = props.values.length > 0 ? props.values : [0];
29039
+ const width = 112;
29040
+ const height = 32;
29041
+ const max = Math.max(...values, 0, 1);
29042
+ const linePath = props.kind === "line" ? buildLineChartPath(values, width, height) : "";
29043
+ const areaPath = props.kind === "line" ? buildLineChartArea(values, width, height) : "";
29044
+ return /* @__PURE__ */ jsx2("div", { className: `ghsync-widget__trend ghsync-widget__trend--${props.tone}`, "aria-hidden": "true", children: /* @__PURE__ */ jsx2("svg", { viewBox: `0 0 ${width} ${height}`, focusable: "false", children: props.kind === "line" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
29045
+ areaPath ? /* @__PURE__ */ jsx2("path", { d: areaPath, className: "ghsync-widget__trend-area" }) : null,
29046
+ linePath ? /* @__PURE__ */ jsx2("path", { d: linePath, className: "ghsync-widget__trend-line" }) : null
29047
+ ] }) : values.map((value, index2) => {
29048
+ const barWidth = width / values.length;
29049
+ const x = index2 * barWidth + 1;
29050
+ const barHeight = Math.max(3, value / max * (height - 4));
29051
+ const y = height - barHeight - 1;
29052
+ return /* @__PURE__ */ jsx2(
29053
+ "rect",
29054
+ {
29055
+ x,
29056
+ y,
29057
+ width: Math.max(2, barWidth - 3),
29058
+ height: barHeight,
29059
+ rx: 2,
29060
+ className: "ghsync-widget__trend-bar"
29061
+ },
29062
+ `${index2}-${value}`
29063
+ );
29064
+ }) }) });
29065
+ }
28759
29066
  function buildThemeVars(theme, themeMode) {
28760
29067
  return {
28761
29068
  colorScheme: themeMode,
@@ -33195,15 +33502,19 @@ function GitHubSyncDashboardWidget() {
33195
33502
  runningSync,
33196
33503
  scheduleFrequencyMinutes
33197
33504
  });
33198
- const syncProgress = getRunningSyncProgressModel(displaySyncState);
33199
33505
  const syncMetricCards = getSyncMetricCards({
33200
33506
  totalSyncedIssuesCount: current.totalSyncedIssuesCount,
33201
33507
  erroredIssuesCount: displaySyncState.erroredIssuesCount,
33202
33508
  syncState: displaySyncState,
33203
33509
  savedMappingCount
33204
33510
  });
33511
+ const syncProgress = getRunningSyncProgressModel(displaySyncState);
33205
33512
  const lastSync = formatDate(displaySyncState.checkedAt, "Never");
33206
33513
  const armSyncCompletionToast = useSyncCompletionToast(displaySyncState, toast);
33514
+ const widgetStatusSummary = showInitialLoadingState ? "Fetching the latest GitHub sync state from the worker." : syncProgress ? [
33515
+ syncProgress.issueProgressLabel,
33516
+ syncProgress.currentIssueLabel ?? syncProgress.repositoryPosition
33517
+ ].filter((value) => Boolean(value)).join(" \xB7 ") : syncSetupIssue === "missing_token" ? "Open settings to validate GitHub access." : syncSetupIssue === "missing_mapping" ? "Open settings and add a repository. The Paperclip project will be created if it does not exist." : syncSetupIssue === "missing_board_access" ? hasCompanyContext ? "Open settings and connect Paperclip board access before running sync." : "Open plugin settings inside a company to connect required Paperclip board access." : displaySyncState.message ?? (displaySyncState.checkedAt ? `Last checked ${lastSync}.` : `Automatic sync runs ${scheduleDescription}.`);
33207
33518
  useEffect2(() => {
33208
33519
  if (settings.data) {
33209
33520
  setCachedSettings(settings.data);
@@ -33232,41 +33543,41 @@ function GitHubSyncDashboardWidget() {
33232
33543
  if (displaySyncState.status !== "running") {
33233
33544
  return;
33234
33545
  }
33235
- const refreshSettings = () => {
33546
+ const intervalId = globalThis.setInterval(() => {
33236
33547
  try {
33237
33548
  settings.refresh();
33238
33549
  } catch {
33239
33550
  return;
33240
33551
  }
33241
- };
33242
- const intervalId = globalThis.setInterval(() => {
33243
- refreshSettings();
33244
33552
  }, SYNC_POLL_INTERVAL_MS);
33245
- refreshSettings();
33553
+ try {
33554
+ settings.refresh();
33555
+ } catch {
33556
+ }
33246
33557
  return () => {
33247
33558
  globalThis.clearInterval(intervalId);
33248
33559
  };
33249
33560
  }, [displaySyncState.status, settings.refresh]);
33250
33561
  useEffect2(() => {
33251
- const refreshSettings = () => {
33562
+ if (typeof window === "undefined" || typeof document === "undefined") {
33563
+ return;
33564
+ }
33565
+ const refreshWidgetData = () => {
33252
33566
  try {
33253
33567
  settings.refresh();
33254
33568
  } catch {
33255
33569
  return;
33256
33570
  }
33257
33571
  };
33258
- if (typeof window === "undefined" || typeof document === "undefined") {
33259
- return;
33260
- }
33261
33572
  const handleSettingsUpdated = () => {
33262
- refreshSettings();
33573
+ refreshWidgetData();
33263
33574
  };
33264
33575
  const handleWindowFocus = () => {
33265
- refreshSettings();
33576
+ refreshWidgetData();
33266
33577
  };
33267
33578
  const handleVisibilityChange = () => {
33268
33579
  if (document.visibilityState === "visible") {
33269
- refreshSettings();
33580
+ refreshWidgetData();
33270
33581
  }
33271
33582
  };
33272
33583
  window.addEventListener(GITHUB_SYNC_SETTINGS_UPDATED_EVENT, handleSettingsUpdated);
@@ -33299,7 +33610,7 @@ function GitHubSyncDashboardWidget() {
33299
33610
  });
33300
33611
  armSyncCompletionToast(nextSyncState);
33301
33612
  notifyGitHubSyncSettingsChanged();
33302
- await settings.refresh();
33613
+ await Promise.resolve().then(() => settings.refresh());
33303
33614
  } catch (error) {
33304
33615
  const message = getActionErrorMessage(error, "Unable to run GitHub sync.");
33305
33616
  setManualSyncRequestError(message);
@@ -33309,7 +33620,7 @@ function GitHubSyncDashboardWidget() {
33309
33620
  tone: "error"
33310
33621
  });
33311
33622
  try {
33312
- await settings.refresh();
33623
+ await Promise.resolve().then(() => settings.refresh());
33313
33624
  } catch {
33314
33625
  return;
33315
33626
  }
@@ -33333,7 +33644,7 @@ function GitHubSyncDashboardWidget() {
33333
33644
  });
33334
33645
  armSyncCompletionToast(nextSyncState);
33335
33646
  notifyGitHubSyncSettingsChanged();
33336
- await settings.refresh();
33647
+ await Promise.resolve().then(() => settings.refresh());
33337
33648
  } catch (error) {
33338
33649
  const message = getActionErrorMessage(error, "Unable to cancel GitHub sync.");
33339
33650
  setManualSyncRequestError(message);
@@ -33378,40 +33689,42 @@ function GitHubSyncDashboardWidget() {
33378
33689
  ] })
33379
33690
  ] }),
33380
33691
  settings.error ? /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__message", children: settings.error.message }) : null,
33381
- /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__stats", children: syncMetricCards.map((metric) => /* @__PURE__ */ jsxs2(
33382
- "div",
33383
- {
33384
- className: `ghsync-widget__stat${metric.emphasized ? " ghsync-widget__stat--emphasized" : ""}`,
33385
- children: [
33386
- /* @__PURE__ */ jsx2("strong", { children: showInitialLoadingState ? /* @__PURE__ */ jsx2(LoadingSpinner, { size: "sm", label: `Loading ${metric.label.toLowerCase()}` }) : metric.value }),
33387
- /* @__PURE__ */ jsx2("span", { children: metric.label }),
33388
- /* @__PURE__ */ jsx2("p", { children: showInitialLoadingState ? "Loading current sync data." : metric.description })
33389
- ]
33390
- },
33391
- metric.key
33392
- )) }),
33393
- /* @__PURE__ */ jsx2(
33692
+ /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__stats", children: syncMetricCards.map((card) => {
33693
+ const tone = getSyncMetricCardTone(card);
33694
+ return /* @__PURE__ */ jsxs2(
33695
+ "div",
33696
+ {
33697
+ className: `ghsync-widget__stat ghsync-widget__stat--${tone}`,
33698
+ children: [
33699
+ /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__stat-top", children: /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__stat-value", children: [
33700
+ /* @__PURE__ */ jsx2("span", { className: "ghsync-widget__stat-label", children: card.label }),
33701
+ /* @__PURE__ */ jsx2("strong", { children: showInitialLoadingState ? /* @__PURE__ */ jsx2(LoadingSpinner, { size: "sm", label: `Loading ${card.label.toLowerCase()}` }) : String(card.value) })
33702
+ ] }) }),
33703
+ /* @__PURE__ */ jsx2("p", { className: `ghsync-widget__stat-change ghsync-widget__stat-change--${tone}`, children: showInitialLoadingState ? "Loading sync summary." : card.description })
33704
+ ]
33705
+ },
33706
+ card.key
33707
+ );
33708
+ }) }),
33709
+ syncInFlight ? /* @__PURE__ */ jsx2(
33394
33710
  SyncProgressPanel,
33395
33711
  {
33396
33712
  syncState: displaySyncState,
33397
33713
  compact: true
33398
33714
  }
33399
- ),
33715
+ ) : null,
33400
33716
  /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__summary", children: [
33401
33717
  /* @__PURE__ */ jsx2("strong", { children: showInitialLoadingState ? "Loading sync status\u2026" : syncInFlight ? "Live run" : syncUnlocked ? "Latest result" : "Next step" }),
33402
- /* @__PURE__ */ jsx2("span", { children: showInitialLoadingState ? "Fetching the latest GitHub sync state from the worker." : syncProgress ? [
33403
- syncProgress.issueProgressLabel,
33404
- syncProgress.currentIssueLabel ?? syncProgress.repositoryPosition
33405
- ].filter((value) => Boolean(value)).join(" \xB7 ") : syncSetupIssue === "missing_token" ? "Open settings to validate GitHub access." : syncSetupIssue === "missing_mapping" ? "Open settings and add a repository. The Paperclip project will be created if it does not exist." : syncSetupIssue === "missing_board_access" ? hasCompanyContext ? "Open settings and connect Paperclip board access before running sync." : "Open plugin settings inside a company to connect required Paperclip board access." : displaySyncState.checkedAt ? `Last checked ${lastSync}.` : "Everything is configured. Run the first sync when you are ready." })
33718
+ /* @__PURE__ */ jsx2("span", { children: widgetStatusSummary })
33406
33719
  ] }),
33407
- /* @__PURE__ */ jsx2(
33720
+ manualSyncRequestError || displaySyncState.status === "error" || Boolean(displaySyncState.recentFailures?.length) ? /* @__PURE__ */ jsx2(
33408
33721
  SyncDiagnosticsPanel,
33409
33722
  {
33410
33723
  syncState: displaySyncState,
33411
33724
  requestError: manualSyncRequestError,
33412
33725
  compact: true
33413
33726
  }
33414
- ),
33727
+ ) : null,
33415
33728
  /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__actions", children: /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__button-row", children: [
33416
33729
  /* @__PURE__ */ jsx2(
33417
33730
  "a",
@@ -33445,6 +33758,230 @@ function GitHubSyncDashboardWidget() {
33445
33758
  ] })
33446
33759
  ] });
33447
33760
  }
33761
+ function GitHubSyncKpiDashboardWidget() {
33762
+ const hostContext = useHostContext();
33763
+ const settings = usePluginData(
33764
+ "settings.registration",
33765
+ hostContext.companyId ? { companyId: hostContext.companyId } : {}
33766
+ );
33767
+ const dashboardMetrics = usePluginData(
33768
+ "dashboard.metrics",
33769
+ hostContext.companyId ? { companyId: hostContext.companyId } : {}
33770
+ );
33771
+ const [settingsHref, setSettingsHref] = useState2(SETTINGS_INDEX_HREF2);
33772
+ const [cachedSettings, setCachedSettings] = useState2(null);
33773
+ const [cachedDashboardMetrics, setCachedDashboardMetrics] = useState2(null);
33774
+ const themeMode = useResolvedThemeMode();
33775
+ const boardAccessRequirement = usePaperclipBoardAccessRequirement();
33776
+ const theme = themeMode === "light" ? LIGHT_PALETTE : DARK_PALETTE;
33777
+ const themeVars = buildThemeVars(theme, themeMode);
33778
+ const current = settings.data ?? cachedSettings ?? EMPTY_SETTINGS;
33779
+ const currentDashboardMetrics = dashboardMetrics.data ?? cachedDashboardMetrics ?? EMPTY_DASHBOARD_METRICS;
33780
+ const showInitialLoadingState = settings.loading && !settings.data && !cachedSettings;
33781
+ const syncState = current.syncState ?? EMPTY_SETTINGS.syncState;
33782
+ const tokenValid = Boolean(current.githubTokenConfigured);
33783
+ const hasCompanyContext = Boolean(hostContext.companyId);
33784
+ const showInitialKpiLoadingState = hasCompanyContext && dashboardMetrics.loading && !dashboardMetrics.data && !cachedDashboardMetrics;
33785
+ const boardAccessConfigured = Boolean(current.paperclipBoardAccessConfigured);
33786
+ const boardAccessRequired = boardAccessRequirement.required;
33787
+ const boardAccessReady = !boardAccessRequired || hasCompanyContext && boardAccessConfigured;
33788
+ const savedMappingCount = getComparableMappings(current.mappings ?? []).length;
33789
+ const syncSetupIssue = getSyncSetupIssue({
33790
+ tokenStatus: tokenValid ? "valid" : "required",
33791
+ savedMappingCount,
33792
+ boardAccessRequired,
33793
+ boardAccessConfigured,
33794
+ hasCompanyContext
33795
+ });
33796
+ const displaySyncState = getDisplaySyncState(syncState, {
33797
+ hasToken: tokenValid,
33798
+ hasMappings: savedMappingCount > 0,
33799
+ hasBoardAccess: boardAccessReady
33800
+ });
33801
+ const syncPersistedRunning = displaySyncState.status === "running";
33802
+ const scheduleDescription = formatScheduleFrequency(normalizeScheduleFrequencyMinutes(current.scheduleFrequencyMinutes));
33803
+ const kpiSummary = getKpiDashboardSummary({
33804
+ hasCompanyContext,
33805
+ metrics: currentDashboardMetrics,
33806
+ syncState: displaySyncState,
33807
+ syncIssue: syncSetupIssue
33808
+ });
33809
+ const kpiCards = buildDashboardKpiCards({
33810
+ metrics: currentDashboardMetrics,
33811
+ hasCompanyContext
33812
+ });
33813
+ const syncProgress = getRunningSyncProgressModel(displaySyncState);
33814
+ const lastSync = formatDate(displaySyncState.checkedAt, "Never");
33815
+ const widgetStatusSummary = showInitialLoadingState ? "Loading KPI status." : syncProgress ? [
33816
+ syncProgress.issueProgressLabel,
33817
+ syncProgress.currentIssueLabel ?? syncProgress.repositoryPosition
33818
+ ].filter((value) => Boolean(value)).join(" \xB7 ") : syncSetupIssue === "missing_token" ? "Finish setup to refresh KPI history." : syncSetupIssue === "missing_board_access" ? "Connect board access to refresh KPI history." : !hasCompanyContext ? "Open in a company dashboard." : currentDashboardMetrics.status === "no_mappings" ? "Add a mapped repository." : !currentDashboardMetrics.notes.backlogHistoryAvailable && !currentDashboardMetrics.notes.activityHistoryAvailable ? "Run a full sync to seed KPI history." : [
33819
+ currentDashboardMetrics.backlog.lastCapturedAt ? `Backlog ${formatDate(currentDashboardMetrics.backlog.lastCapturedAt, currentDashboardMetrics.backlog.lastCapturedAt)}` : null,
33820
+ currentDashboardMetrics.githubIssuesClosed.lastRecordedAt ? `Activity ${formatDate(currentDashboardMetrics.githubIssuesClosed.lastRecordedAt, currentDashboardMetrics.githubIssuesClosed.lastRecordedAt)}` : null
33821
+ ].filter((value) => Boolean(value)).join(" \xB7 ") || (displaySyncState.checkedAt ? `Last sync ${lastSync}` : `Auto-sync ${scheduleDescription}`);
33822
+ useEffect2(() => {
33823
+ if (settings.data) {
33824
+ setCachedSettings(settings.data);
33825
+ }
33826
+ }, [settings.data]);
33827
+ useEffect2(() => {
33828
+ if (dashboardMetrics.data) {
33829
+ setCachedDashboardMetrics(dashboardMetrics.data);
33830
+ }
33831
+ }, [dashboardMetrics.data]);
33832
+ useEffect2(() => {
33833
+ let cancelled = false;
33834
+ async function loadSettingsHref() {
33835
+ try {
33836
+ const plugins = await fetchJson("/api/plugins");
33837
+ if (!cancelled) {
33838
+ setSettingsHref(resolvePluginSettingsHref(plugins));
33839
+ }
33840
+ } catch {
33841
+ if (!cancelled) {
33842
+ setSettingsHref(SETTINGS_INDEX_HREF2);
33843
+ }
33844
+ }
33845
+ }
33846
+ void loadSettingsHref();
33847
+ return () => {
33848
+ cancelled = true;
33849
+ };
33850
+ }, []);
33851
+ useEffect2(() => {
33852
+ if (displaySyncState.status !== "running") {
33853
+ return;
33854
+ }
33855
+ const refreshWidgetData = () => {
33856
+ try {
33857
+ settings.refresh();
33858
+ } catch {
33859
+ }
33860
+ try {
33861
+ dashboardMetrics.refresh();
33862
+ } catch {
33863
+ return;
33864
+ }
33865
+ };
33866
+ const intervalId = globalThis.setInterval(() => {
33867
+ refreshWidgetData();
33868
+ }, SYNC_POLL_INTERVAL_MS);
33869
+ refreshWidgetData();
33870
+ return () => {
33871
+ globalThis.clearInterval(intervalId);
33872
+ };
33873
+ }, [dashboardMetrics.refresh, displaySyncState.status, settings.refresh]);
33874
+ useEffect2(() => {
33875
+ const refreshWidgetData = () => {
33876
+ try {
33877
+ settings.refresh();
33878
+ } catch {
33879
+ }
33880
+ try {
33881
+ dashboardMetrics.refresh();
33882
+ } catch {
33883
+ return;
33884
+ }
33885
+ };
33886
+ if (typeof window === "undefined" || typeof document === "undefined") {
33887
+ return;
33888
+ }
33889
+ const handleSettingsUpdated = () => {
33890
+ refreshWidgetData();
33891
+ };
33892
+ const handleWindowFocus = () => {
33893
+ refreshWidgetData();
33894
+ };
33895
+ const handleVisibilityChange = () => {
33896
+ if (document.visibilityState === "visible") {
33897
+ refreshWidgetData();
33898
+ }
33899
+ };
33900
+ window.addEventListener(GITHUB_SYNC_SETTINGS_UPDATED_EVENT, handleSettingsUpdated);
33901
+ window.addEventListener("focus", handleWindowFocus);
33902
+ document.addEventListener("visibilitychange", handleVisibilityChange);
33903
+ return () => {
33904
+ window.removeEventListener(GITHUB_SYNC_SETTINGS_UPDATED_EVENT, handleSettingsUpdated);
33905
+ window.removeEventListener("focus", handleWindowFocus);
33906
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
33907
+ };
33908
+ }, [dashboardMetrics.refresh, settings.refresh]);
33909
+ return /* @__PURE__ */ jsxs2("section", { className: "ghsync-widget", style: themeVars, children: [
33910
+ /* @__PURE__ */ jsx2("style", { children: WIDGET_STYLES }),
33911
+ /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__card", children: [
33912
+ /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__top", children: [
33913
+ /* @__PURE__ */ jsxs2("div", { children: [
33914
+ /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__eyebrow", children: "GitHub KPIs" }),
33915
+ /* @__PURE__ */ jsx2("h3", { children: kpiSummary.title }),
33916
+ /* @__PURE__ */ jsx2("p", { children: kpiSummary.body }),
33917
+ /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__meta", children: [
33918
+ /* @__PURE__ */ jsxs2("span", { children: [
33919
+ savedMappingCount,
33920
+ " ",
33921
+ savedMappingCount === 1 ? "repository" : "repositories"
33922
+ ] }),
33923
+ /* @__PURE__ */ jsx2("span", { className: "ghsync-widget__meta-dot", "aria-hidden": "true" }),
33924
+ /* @__PURE__ */ jsxs2("span", { children: [
33925
+ "Auto-sync ",
33926
+ scheduleDescription
33927
+ ] }),
33928
+ /* @__PURE__ */ jsx2("span", { className: "ghsync-widget__meta-dot", "aria-hidden": "true" }),
33929
+ /* @__PURE__ */ jsxs2("span", { children: [
33930
+ "Last sync ",
33931
+ lastSync
33932
+ ] })
33933
+ ] })
33934
+ ] }),
33935
+ /* @__PURE__ */ jsxs2("span", { className: `ghsync__badge ${getToneClass(kpiSummary.tone)}`, children: [
33936
+ /* @__PURE__ */ jsx2("span", { className: "ghsync__badge-dot", "aria-hidden": "true" }),
33937
+ kpiSummary.label
33938
+ ] })
33939
+ ] }),
33940
+ settings.error ? /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__message", children: settings.error.message }) : null,
33941
+ dashboardMetrics.error ? /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__message", children: dashboardMetrics.error.message }) : null,
33942
+ /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__stats", children: kpiCards.map((card) => /* @__PURE__ */ jsxs2(
33943
+ "div",
33944
+ {
33945
+ className: `ghsync-widget__stat ghsync-widget__stat--${card.tone}`,
33946
+ children: [
33947
+ /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__stat-top", children: [
33948
+ /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__stat-value", children: [
33949
+ /* @__PURE__ */ jsx2("span", { className: "ghsync-widget__stat-label", children: card.title }),
33950
+ /* @__PURE__ */ jsx2("strong", { children: showInitialKpiLoadingState ? /* @__PURE__ */ jsx2(LoadingSpinner, { size: "sm", label: `Loading ${card.title.toLowerCase()}` }) : card.valueLabel })
33951
+ ] }),
33952
+ /* @__PURE__ */ jsx2(
33953
+ DashboardTrendGraphic,
33954
+ {
33955
+ values: card.history,
33956
+ tone: card.tone,
33957
+ kind: card.chartKind
33958
+ }
33959
+ )
33960
+ ] }),
33961
+ /* @__PURE__ */ jsx2("p", { className: `ghsync-widget__stat-change ghsync-widget__stat-change--${card.tone}`, children: showInitialKpiLoadingState ? "Loading KPI history." : card.changeLabel }),
33962
+ /* @__PURE__ */ jsx2("p", { className: "ghsync-widget__stat-note", children: showInitialKpiLoadingState ? "Fetching the latest company KPI data." : card.note })
33963
+ ]
33964
+ },
33965
+ card.key
33966
+ )) }),
33967
+ /* @__PURE__ */ jsxs2("div", { className: "ghsync-widget__summary", children: [
33968
+ /* @__PURE__ */ jsx2("strong", { children: showInitialLoadingState ? "Status" : syncPersistedRunning ? "Live sync" : "Latest snapshot" }),
33969
+ /* @__PURE__ */ jsx2("span", { children: widgetStatusSummary })
33970
+ ] }),
33971
+ /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__actions", children: /* @__PURE__ */ jsx2("div", { className: "ghsync-widget__button-row", children: /* @__PURE__ */ jsx2(
33972
+ "a",
33973
+ {
33974
+ href: settingsHref,
33975
+ className: getPluginActionClassName({
33976
+ variant: "secondary",
33977
+ extraClassName: "ghsync-widget__link"
33978
+ }),
33979
+ children: "Open settings"
33980
+ }
33981
+ ) }) })
33982
+ ] })
33983
+ ] });
33984
+ }
33448
33985
  function GitHubMarkIcon(props) {
33449
33986
  return /* @__PURE__ */ jsx2(
33450
33987
  "svg",
@@ -34217,6 +34754,7 @@ export {
34217
34754
  GitHubSyncGlobalToolbarButton,
34218
34755
  GitHubSyncIssueDetailTab,
34219
34756
  GitHubSyncIssueTaskDetailView,
34757
+ GitHubSyncKpiDashboardWidget,
34220
34758
  GitHubSyncProjectPullRequestsPage,
34221
34759
  GitHubSyncProjectPullRequestsSidebarItem,
34222
34760
  GitHubSyncSettingsPage,