@runfusion/fusion 0.26.0 → 0.27.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.
Files changed (174) hide show
  1. package/dist/bin.js +12847 -2514
  2. package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
  3. package/dist/client/assets/AgentDetailView-shgiiUb4.js +18 -0
  4. package/dist/client/assets/{AgentsView-CV3vm7Qk.css → AgentsView-B3ADnF0D.css} +1 -1
  5. package/dist/client/assets/{AgentsView-D6Zi5zfP.js → AgentsView-CpwqOVDz.js} +12 -7
  6. package/dist/client/assets/ChatView-DyRBOIKL.js +1 -0
  7. package/dist/client/assets/{DevServerView--_WBvIDQ.js → DevServerView-Cdelj9-m.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-xedtR-Rd.js → DirectoryPicker-C0kmRv0u.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-Bg2oaZks.js → DocumentsView-B94U9ijs.js} +1 -1
  10. package/dist/client/assets/{EvalsView-B3uOCXfr.js → EvalsView-O_4YWy--.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-Bx6yXVS5.js → ExperimentalAgentOnboardingModal-CkEiF85-.js} +1 -1
  12. package/dist/client/assets/InsightsView-D-Qe0tRr.js +11 -0
  13. package/dist/client/assets/{MemoryView-xcN_eouf.js → MemoryView-CoRUmRvb.js} +2 -2
  14. package/dist/client/assets/NodesView-DQzXjcLc.js +14 -0
  15. package/dist/client/assets/{PiExtensionsManager-Cc8aAZXg.js → PiExtensionsManager-Dn1LmFbq.js} +2 -2
  16. package/dist/client/assets/PluginManager-Y0fs-6No.js +1 -0
  17. package/dist/client/assets/{ResearchView-CERNf7sJ.js → ResearchView-CjOxKhdS.js} +1 -1
  18. package/dist/client/assets/{SettingsModal-B1r0yASu.js → SettingsModal-Bg1-3JO_.js} +1 -1
  19. package/dist/client/assets/{SettingsModal-Cis-4Lot.css → SettingsModal-Ci0_sqbU.css} +1 -1
  20. package/dist/client/assets/SettingsModal-DL7tjJQa.js +31 -0
  21. package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
  22. package/dist/client/assets/{SetupWizardModal-D1q548_L.js → SetupWizardModal-DuzYPbuJ.js} +1 -1
  23. package/dist/client/assets/{SkillsView-ClLM6u6p.js → SkillsView-BIFoVNUf.js} +1 -1
  24. package/dist/client/assets/{StashRecoveryView-ze0pEZ5U.js → StashRecoveryView-C52KsV7f.js} +1 -1
  25. package/dist/client/assets/{TodoView-CTmIfy2M.js → TodoView-sS_mT0Y7.js} +2 -2
  26. package/dist/client/assets/{dashboard-view-CyWN-d02.js → dashboard-view-BWGH_fAq.js} +1 -1
  27. package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
  28. package/dist/client/assets/dashboard-view-MB-86hAu.js +21 -0
  29. package/dist/client/assets/{folder-open-BZuKESeq.js → folder-open-B9cwJ-OX.js} +1 -1
  30. package/dist/client/assets/index-BOjPRqEk.js +692 -0
  31. package/dist/client/assets/index-BmSEq8Rb.css +1 -0
  32. package/dist/client/assets/{star-D75YKEq-.js → star-BDn04UYV.js} +1 -1
  33. package/dist/client/assets/{upload-BYYTgWFj.js → upload-zdPPycKQ.js} +1 -1
  34. package/dist/client/assets/{users-RS90Aii3.js → users-CPYZjK2g.js} +1 -1
  35. package/dist/client/index.html +2 -2
  36. package/dist/client/version.json +1 -1
  37. package/dist/droid-cli/package.json +1 -1
  38. package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
  39. package/dist/extension.js +7433 -1920
  40. package/dist/pi-claude-cli/package.json +1 -1
  41. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
  42. package/dist/pi-claude-cli/src/provider.ts +7 -1
  43. package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +19 -1
  44. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +20 -2
  45. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
  46. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
  47. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
  48. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
  49. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
  50. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
  51. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
  52. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
  53. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
  54. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
  55. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +21 -5
  56. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
  57. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
  58. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
  59. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
  60. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
  61. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
  62. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
  63. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
  64. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
  65. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
  66. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
  67. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
  68. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
  69. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +46 -2
  70. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
  71. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
  72. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
  73. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
  74. package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
  75. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
  76. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
  77. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
  78. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
  79. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
  80. package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
  81. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
  82. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
  83. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
  84. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
  85. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
  86. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
  87. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
  88. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
  89. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  90. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  91. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  92. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  93. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  94. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  95. package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
  96. package/dist/plugins/fusion-plugin-reports/package.json +18 -2
  97. package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
  98. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
  99. package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
  100. package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
  101. package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
  102. package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
  103. package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
  104. package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
  105. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
  106. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
  107. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
  108. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
  109. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
  110. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
  111. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
  112. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
  113. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
  114. package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
  115. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
  116. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
  117. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
  118. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
  119. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
  120. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
  121. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
  122. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
  123. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
  124. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
  125. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
  126. package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
  127. package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
  128. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
  129. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
  130. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
  131. package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
  132. package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
  133. package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
  134. package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
  135. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
  136. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
  137. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
  138. package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
  139. package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
  140. package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
  141. package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
  142. package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
  143. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
  144. package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
  145. package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
  146. package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
  147. package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
  148. package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
  149. package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
  150. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
  151. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
  152. package/dist/plugins/fusion-plugin-roadmap/bundled.js +1528 -29391
  153. package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
  154. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  155. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  156. package/package.json +1 -1
  157. package/skill/fusion/SKILL.md +1 -1
  158. package/skill/fusion/references/engine-tools.md +2 -2
  159. package/skill/fusion/references/extension-tools.md +4 -3
  160. package/skill/fusion/references/fusion-capabilities.md +1 -1
  161. package/skill/fusion/workflows/task-management.md +3 -1
  162. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
  163. package/dist/client/assets/AgentDetailView-Cv-vgOj3.js +0 -18
  164. package/dist/client/assets/ChatView-CAHjY9uO.js +0 -1
  165. package/dist/client/assets/InsightsView-Q1zvtF4F.js +0 -11
  166. package/dist/client/assets/NodesView-RxXg58_Q.js +0 -14
  167. package/dist/client/assets/PluginManager-BEkyBajl.js +0 -1
  168. package/dist/client/assets/SettingsModal-BLsac7CJ.js +0 -31
  169. package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
  170. package/dist/client/assets/dashboard-view-4xAN3yO5.js +0 -21
  171. package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
  172. package/dist/client/assets/index-Bdw6llW6.js +0 -692
  173. package/dist/client/assets/index-CZGlyJuS.css +0 -1
  174. package/dist/plugins/fusion-plugin-roadmap/bundled.css +0 -1093
@@ -0,0 +1,23 @@
1
+ /**
2
+ * INTERIM SEAM — replaced by FN-3780's real aggregation orchestrator.
3
+ * Keep the type surface stable so FN-3780 can swap the implementation without renaming exports.
4
+ */
5
+ import type { ReportsCadence } from "./cadence.js";
6
+
7
+ export interface ReportAggregationInput {
8
+ runId: string;
9
+ cadence: ReportsCadence;
10
+ settings: Record<string, unknown>;
11
+ }
12
+
13
+ export interface ReportAggregationOutput {
14
+ summary: string;
15
+ sections: Array<{ id: string; title: string; body: string }>;
16
+ }
17
+
18
+ export type ReportsAggregator = (input: ReportAggregationInput) => Promise<ReportAggregationOutput>;
19
+
20
+ export const aggregateReportData: ReportsAggregator = async ({ cadence }) => ({
21
+ summary: `Aggregation scaffold not yet implemented for ${cadence} reports.`,
22
+ sections: [],
23
+ });
@@ -0,0 +1,97 @@
1
+ import type { Report, ReportStatus } from "./store/report-types.js";
2
+
3
+ export type ApprovalState = "not_required" | "awaiting_approval" | "approved" | "rejected" | "published";
4
+ export type ApprovalAction = "approve" | "reject" | "publish";
5
+
6
+ export interface ApprovalDecision {
7
+ decidedBy: string;
8
+ decidedAt: string;
9
+ note?: string;
10
+ action: ApprovalAction;
11
+ }
12
+
13
+ export interface ApprovalSettings {
14
+ approvalRequired: boolean;
15
+ autoPublishOnApproval: boolean;
16
+ approverAgentIds: string[];
17
+ publishTargets: string[];
18
+ }
19
+
20
+ export interface ApprovalActor {
21
+ id: string;
22
+ type: "human" | "agent";
23
+ }
24
+
25
+ export type ApprovalError = "invalid_transition" | "unauthorized";
26
+
27
+ export function initializeApprovalState(reportStatus: ReportStatus, settings: ApprovalSettings): ApprovalState {
28
+ if (reportStatus !== "review_complete") return "not_required";
29
+ if (settings.approvalRequired) return "awaiting_approval";
30
+ return settings.autoPublishOnApproval ? "published" : "approved";
31
+ }
32
+
33
+ export function nextApprovalState(
34
+ current: ApprovalState,
35
+ action: ApprovalAction,
36
+ settings: ApprovalSettings,
37
+ actor: ApprovalActor,
38
+ ): { next: ApprovalState } | { error: ApprovalError } {
39
+ if (!isAuthorized(settings, actor)) return { error: "unauthorized" };
40
+ if (current === "awaiting_approval" && action === "approve") {
41
+ return { next: settings.autoPublishOnApproval ? "published" : "approved" };
42
+ }
43
+ if (current === "awaiting_approval" && action === "reject") return { next: "rejected" };
44
+ if (current === "approved" && action === "publish") return { next: "published" };
45
+ return { error: "invalid_transition" };
46
+ }
47
+
48
+ export function applyDecision(
49
+ report: Report,
50
+ decision: ApprovalDecision,
51
+ settings: ApprovalSettings,
52
+ actor: ApprovalActor,
53
+ ):
54
+ | { error: ApprovalError }
55
+ | {
56
+ updatedReport: Partial<Report>;
57
+ sideEffects: { publishTargets: string[] };
58
+ } {
59
+ const transition = nextApprovalState(report.approvalState, decision.action, settings, actor);
60
+ if ("error" in transition) return transition;
61
+
62
+ const approvalHistory = [...report.approvalHistory, decision];
63
+ const update: Partial<Report> = {
64
+ approvalState: transition.next,
65
+ approvalHistory,
66
+ };
67
+
68
+ if (transition.next === "approved") {
69
+ update.status = "approved";
70
+ update.approvedAt = decision.decidedAt;
71
+ update.approvedBy = decision.decidedBy;
72
+ }
73
+
74
+ if (transition.next === "published") {
75
+ update.status = "published";
76
+ update.publishedAt = decision.decidedAt;
77
+ if (decision.action === "approve") {
78
+ update.approvedAt = decision.decidedAt;
79
+ update.approvedBy = decision.decidedBy;
80
+ }
81
+ }
82
+
83
+ return {
84
+ updatedReport: update,
85
+ sideEffects: {
86
+ publishTargets: transition.next === "published" ? [...settings.publishTargets] : [],
87
+ },
88
+ };
89
+ }
90
+
91
+ function isAuthorized(settings: ApprovalSettings, actor: ApprovalActor): boolean {
92
+ if (!settings.approvalRequired) return true;
93
+ const approvers = settings.approverAgentIds;
94
+ if (approvers.length === 0) return actor.type === "human";
95
+ if (actor.type !== "agent") return false;
96
+ return approvers.includes(actor.id);
97
+ }
@@ -0,0 +1,23 @@
1
+ import { getDailyEnabled, getTimezone, getWeeklyEnabled } from "./settings.js";
2
+
3
+ export type ReportsCadence = "daily" | "weekly";
4
+
5
+ export interface CadenceResolution {
6
+ cadence: ReportsCadence;
7
+ timezone: string;
8
+ }
9
+
10
+ export function resolveEnabledCadences(settings: Record<string, unknown>): CadenceResolution[] {
11
+ const timezone = getTimezone(settings);
12
+ const enabled: CadenceResolution[] = [];
13
+
14
+ if (getDailyEnabled(settings)) {
15
+ enabled.push({ cadence: "daily", timezone });
16
+ }
17
+
18
+ if (getWeeklyEnabled(settings)) {
19
+ enabled.push({ cadence: "weekly", timezone });
20
+ }
21
+
22
+ return enabled;
23
+ }
@@ -0,0 +1,82 @@
1
+ .reports-view {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--space-md);
5
+ padding: var(--space-lg);
6
+ }
7
+ .reports-view-header { display: flex; justify-content: space-between; align-items: center; }
8
+ .reports-filters { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--space-sm); }
9
+ .reports-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: var(--space-md); }
10
+ .reports-list { display: flex; flex-direction: column; gap: var(--space-sm); }
11
+ .reports-list-item {
12
+ text-align: left;
13
+ border: var(--btn-border-width) solid var(--border);
14
+ transition: border-color var(--transition-fast), background var(--transition-fast), box-shadow var(--transition-fast);
15
+ }
16
+ .reports-list-item[data-selected="true"] {
17
+ border-color: var(--todo);
18
+ background: var(--card-hover);
19
+ box-shadow: var(--focus-ring-strong);
20
+ }
21
+ .reports-detail { display: flex; flex-direction: column; gap: var(--space-sm); }
22
+ .reports-detail-header { display: flex; justify-content: space-between; align-items: center; gap: var(--space-sm); }
23
+ .reports-detail-body { display: flex; flex-direction: column; gap: var(--space-sm); }
24
+ .reports-detail-meta {
25
+ color: var(--text-muted);
26
+ font-size: calc(var(--space-sm) + (var(--space-xs) * 1.5));
27
+ }
28
+ .reports-detail-sections {
29
+ display: flex;
30
+ flex-wrap: wrap;
31
+ gap: var(--space-sm);
32
+ }
33
+ .reports-detail iframe, .reports-compare iframe { width: 100%; min-height: calc(var(--space-2xl) * 10); border: var(--btn-border-width) solid var(--border); border-radius: var(--radius-md); background: var(--surface); }
34
+ .reports-empty {
35
+ align-items: center;
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: var(--space-sm);
39
+ justify-content: center;
40
+ min-height: calc(var(--space-2xl) * 6);
41
+ text-align: center;
42
+ }
43
+ .reports-empty p {
44
+ color: var(--text-muted);
45
+ margin: 0;
46
+ }
47
+ .reports-compare {
48
+ width: min(100%, calc(var(--space-2xl) * 32));
49
+ max-height: min(100%, calc(100dvh - var(--space-2xl)));
50
+ overflow: auto;
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: var(--space-md);
54
+ }
55
+ .reports-compare-header {
56
+ position: sticky;
57
+ top: 0;
58
+ background: var(--card);
59
+ z-index: 1;
60
+ }
61
+ .reports-compare-pickers {
62
+ display: grid;
63
+ grid-template-columns: repeat(2, minmax(0, 1fr));
64
+ gap: var(--space-sm);
65
+ }
66
+ .reports-compare-frames { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-sm); }
67
+ @media (max-width: 1024px) {
68
+ .reports-filters { grid-template-columns: repeat(3, minmax(0, 1fr)); }
69
+ .reports-compare-pickers,
70
+ .reports-compare-frames { grid-template-columns: minmax(0, 1fr); }
71
+ }
72
+ @media (max-width: 900px) {
73
+ .reports-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
74
+ }
75
+ @media (max-width: 768px) {
76
+ .reports-filters { grid-template-columns: minmax(0, 1fr); }
77
+ .reports-layout { grid-template-columns: minmax(0, 1fr); }
78
+ .reports-compare {
79
+ width: min(100%, calc(100vw - var(--space-lg)));
80
+ max-height: min(100%, calc(100dvh - var(--space-lg)));
81
+ }
82
+ }
@@ -0,0 +1,24 @@
1
+ import "./ReportsView.css";
2
+ import { ReportComparisonDrawer } from "./components/ReportComparisonDrawer.js";
3
+ import { ReportDetailPanel } from "./components/ReportDetailPanel.js";
4
+ import { ReportEmptyState } from "./components/ReportEmptyState.js";
5
+ import { ReportFiltersBar } from "./components/ReportFiltersBar.js";
6
+ import { ReportListItem } from "./components/ReportListItem.js";
7
+ import type { ToastType } from "./types.js";
8
+ import { useReports } from "./useReports.js";
9
+ import { useViewportMode } from "./useViewportMode.js";
10
+
11
+ export function ReportsView({ projectId, addToast }: { projectId?: string; addToast: (message: string, type?: ToastType) => void }) {
12
+ const model = useReports({ projectId, addToast });
13
+ const { mobile } = useViewportMode();
14
+ const agents = [...new Set(model.reports.flatMap((r) => ((r.metadata?.agentIds as string[] | undefined) ?? [])))];
15
+ return <div className="reports-view">
16
+ <div className="reports-view-header"><h2>Reports</h2><button className="btn btn-sm" onClick={model.enterCompareMode}>Compare</button></div>
17
+ <ReportFiltersBar filters={model.filters} onChange={model.setFilters} agents={agents} />
18
+ <div className="reports-layout" data-mobile={mobile ? "true" : "false"}>
19
+ <div className="reports-list">{model.reports.length === 0 ? <ReportEmptyState /> : model.reports.map((report) => <ReportListItem key={report.id} report={report} selected={model.selectedId === report.id} onSelect={model.selectId} />)}</div>
20
+ <ReportDetailPanel report={model.selectedReport} projectId={projectId} />
21
+ </div>
22
+ {model.compareMode ? <ReportComparisonDrawer reports={model.reports} leftId={model.compareA} rightId={model.compareB} onPick={model.setCompareSlot} onClose={model.closeCompareMode} projectId={projectId} /> : null}
23
+ </div>;
24
+ }
@@ -0,0 +1,12 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import * as preview from "../useReportPreview.js";
4
+ import { ReportComparisonDrawer } from "../components/ReportComparisonDrawer.js";
5
+
6
+ describe("ReportComparisonDrawer", () => {
7
+ it("renders compare ui", () => {
8
+ vi.spyOn(preview, "useReportPreview").mockReturnValue({ html: "<article />", loading: false, error: null });
9
+ const { getByText } = render(<ReportComparisonDrawer reports={[{ id: "R-1", title: "A" }, { id: "R-2", title: "B" }] as never} leftId="R-1" rightId="R-2" onPick={vi.fn()} onClose={vi.fn()} />);
10
+ expect(getByText("Compare reports")).toBeInTheDocument();
11
+ });
12
+ });
@@ -0,0 +1,12 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import * as preview from "../useReportPreview.js";
4
+ import { ReportDetailPanel } from "../components/ReportDetailPanel.js";
5
+
6
+ describe("ReportDetailPanel", () => {
7
+ it("renders report", () => {
8
+ vi.spyOn(preview, "useReportPreview").mockReturnValue({ html: "<article />", loading: false, error: null });
9
+ const { getByText } = render(<ReportDetailPanel report={{ id: "R-1", title: "Report", cadence: "daily", status: "published", periodStart: "2026-01-01", periodEnd: "2026-01-02" } as never} />);
10
+ expect(getByText("Report")).toBeInTheDocument();
11
+ });
12
+ });
@@ -0,0 +1,14 @@
1
+ import { fireEvent, render, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ReportFiltersBar } from "../components/ReportFiltersBar.js";
4
+
5
+ const filters = { cadence: "all", status: "all", from: "", to: "", q: "", agentId: "" } as const;
6
+
7
+ describe("ReportFiltersBar", () => {
8
+ it("emits changes", async () => {
9
+ const onChange = vi.fn();
10
+ const { getByPlaceholderText } = render(<ReportFiltersBar filters={{ ...filters }} onChange={onChange} agents={[]} />);
11
+ fireEvent.change(getByPlaceholderText("Search title"), { target: { value: "hello" } });
12
+ await waitFor(() => expect(onChange).toBeCalled());
13
+ });
14
+ });
@@ -0,0 +1,27 @@
1
+ import { fireEvent, render } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import * as reportsHook from "../useReports.js";
4
+ import { ReportsView } from "../ReportsView.js";
5
+
6
+ describe("ReportsView", () => {
7
+ it("renders list and compare toggle", () => {
8
+ vi.spyOn(reportsHook, "useReports").mockReturnValue({
9
+ filters: { cadence: "all", status: "all", from: "", to: "", q: "", agentId: "" },
10
+ setFilters: vi.fn(),
11
+ reports: [{ id: "R-1", title: "A", cadence: "daily", status: "published", periodStart: "2026-01-01", periodEnd: "2026-01-02", metadata: {} }],
12
+ loading: false,
13
+ selectedId: "R-1",
14
+ selectedReport: { id: "R-1", title: "A", cadence: "daily", status: "published", periodStart: "2026-01-01", periodEnd: "2026-01-02", metadata: {} },
15
+ selectId: vi.fn(),
16
+ compareMode: false,
17
+ compareA: undefined,
18
+ compareB: undefined,
19
+ enterCompareMode: vi.fn(),
20
+ closeCompareMode: vi.fn(),
21
+ setCompareSlot: vi.fn(),
22
+ } as never);
23
+ const { getByText } = render(<ReportsView addToast={vi.fn()} />);
24
+ fireEvent.click(getByText("Compare"));
25
+ expect(getByText("Reports")).toBeInTheDocument();
26
+ });
27
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { getReportExportUrl, getReportPreviewHtml, listReports } from "../api.js";
3
+
4
+ describe("api", () => {
5
+ it("lists reports", async () => {
6
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({ reports: [{ id: "R-1" }] }) }));
7
+ const reports = await listReports();
8
+ expect(reports).toHaveLength(1);
9
+ });
10
+
11
+ it("reads preview html", async () => {
12
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, text: async () => "<article/>" }));
13
+ await expect(getReportPreviewHtml("R-1")).resolves.toContain("article");
14
+ });
15
+
16
+ it("builds export url", () => {
17
+ expect(getReportExportUrl("R-1")).toContain("/reports/R-1/export.html");
18
+ });
19
+ });
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { diffReportSections } from "../useReportSectionDiff.js";
3
+
4
+ describe("diffReportSections", () => {
5
+ it("classifies changed sections", () => {
6
+ const a = { metadata: { wins: ["a"] } } as never;
7
+ const b = { metadata: { wins: ["b"] } } as never;
8
+ const diff = diffReportSections(a, b);
9
+ expect(diff.changed.find((item) => item.id === "system-wins")).toBeTruthy();
10
+ });
11
+ });
@@ -0,0 +1,13 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import * as api from "../api.js";
4
+ import { useReports } from "../useReports.js";
5
+
6
+ describe("useReports", () => {
7
+ it("loads reports", async () => {
8
+ vi.spyOn(api, "listReports").mockResolvedValue([{ id: "R-1", title: "A" } as never]);
9
+ vi.spyOn(api, "getReport").mockResolvedValue({ id: "R-1", title: "A" } as never);
10
+ const { result } = renderHook(() => useReports({ addToast: vi.fn() }));
11
+ await waitFor(() => expect(result.current.reports).toHaveLength(1));
12
+ });
13
+ });
@@ -0,0 +1,85 @@
1
+ import type { ReportRecord } from "./types.js";
2
+ import type { ShareBlocks } from "../share-blocks.js";
3
+
4
+ const BASE = "/api/plugins/reports";
5
+
6
+ interface ListReportsParams {
7
+ cadence?: string;
8
+ status?: string;
9
+ from?: string;
10
+ to?: string;
11
+ q?: string;
12
+ agentId?: string;
13
+ projectId?: string;
14
+ }
15
+
16
+ function qp(params: Record<string, string | undefined>): string {
17
+ const entries = Object.entries(params).filter(([, value]) => typeof value === "string" && value.length > 0) as Array<[string, string]>;
18
+ if (entries.length === 0) return "";
19
+ return `?${entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")}`;
20
+ }
21
+
22
+ async function request<T>(path: string, init?: RequestInit, responseType: "json" | "text" = "json"): Promise<T> {
23
+ const response = await fetch(`${BASE}${path}`, init);
24
+ if (!response.ok) {
25
+ let message = `${response.status} ${response.statusText}`;
26
+ try {
27
+ const data = (await response.json()) as { error?: string };
28
+ if (data.error) message = data.error;
29
+ } catch {
30
+ // ignore
31
+ }
32
+ throw new Error(message);
33
+ }
34
+ if (responseType === "text") return (await response.text()) as T;
35
+ return (await response.json()) as T;
36
+ }
37
+
38
+ export async function listReports(params: ListReportsParams = {}): Promise<ReportRecord[]> {
39
+ const data = await request<{ reports: ReportRecord[] }>(`/reports${qp({ ...params })}`);
40
+ return data.reports;
41
+ }
42
+
43
+ export async function getReport(id: string, projectId?: string): Promise<ReportRecord> {
44
+ const data = await request<{ report: ReportRecord }>(`/reports/${encodeURIComponent(id)}${qp({ projectId })}`);
45
+ return data.report;
46
+ }
47
+
48
+ export function getReportPreviewHtml(id: string, projectId?: string): Promise<string> {
49
+ return request<string>(`/reports/${encodeURIComponent(id)}/preview.html${qp({ projectId })}`, undefined, "text");
50
+ }
51
+
52
+ export function getReportExportUrl(id: string, projectId?: string): string {
53
+ return `${BASE}/reports/${encodeURIComponent(id)}/export.html${qp({ projectId })}`;
54
+ }
55
+
56
+ export async function approveReport(id: string, note?: string): Promise<ReportRecord> {
57
+ const data = await request<{ report: ReportRecord }>(`/reports/${encodeURIComponent(id)}/approve`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify(note ? { note } : {}),
61
+ });
62
+ return data.report;
63
+ }
64
+
65
+ export async function rejectReport(id: string, note?: string): Promise<ReportRecord> {
66
+ const data = await request<{ report: ReportRecord }>(`/reports/${encodeURIComponent(id)}/reject`, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify(note ? { note } : {}),
70
+ });
71
+ return data.report;
72
+ }
73
+
74
+ export async function publishReport(id: string): Promise<ReportRecord> {
75
+ const data = await request<{ report: ReportRecord }>(`/reports/${encodeURIComponent(id)}/publish`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: "{}",
79
+ });
80
+ return data.report;
81
+ }
82
+
83
+ export async function getShareBlocks(id: string): Promise<ShareBlocks> {
84
+ return request<ShareBlocks>(`/reports/${encodeURIComponent(id)}/share-blocks`);
85
+ }
@@ -0,0 +1,59 @@
1
+ .report-approval-panel {
2
+ border-top: var(--btn-border-width) solid var(--border);
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--space-md);
6
+ padding-top: var(--space-md);
7
+ }
8
+
9
+ .report-approval-panel__header {
10
+ align-items: center;
11
+ display: flex;
12
+ gap: var(--space-sm);
13
+ justify-content: space-between;
14
+ }
15
+
16
+ .report-approval-panel__header h4 {
17
+ font-size: 0.875rem;
18
+ margin: 0;
19
+ }
20
+
21
+ .report-approval-panel__note {
22
+ min-height: calc(var(--space-2xl) * 2);
23
+ }
24
+
25
+ .card-status-badge--awaiting_approval {
26
+ background: color-mix(in srgb, var(--color-warning) 20%, transparent);
27
+ color: var(--color-warning);
28
+ }
29
+
30
+ .card-status-badge--approved,
31
+ .card-status-badge--published {
32
+ background: color-mix(in srgb, var(--color-success) 20%, transparent);
33
+ color: var(--color-success);
34
+ }
35
+
36
+ .card-status-badge--rejected {
37
+ background: color-mix(in srgb, var(--color-error) 20%, transparent);
38
+ color: var(--color-error);
39
+ }
40
+
41
+ .report-approval-panel__actions {
42
+ display: flex;
43
+ gap: var(--space-sm);
44
+ }
45
+
46
+ .report-approval-panel__history {
47
+ color: var(--text-muted);
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--space-xs);
51
+ margin: 0;
52
+ padding-left: var(--space-lg);
53
+ }
54
+
55
+ @media (max-width: 768px) {
56
+ .report-approval-panel__actions {
57
+ flex-direction: column;
58
+ }
59
+ }
@@ -0,0 +1,58 @@
1
+ import { useMemo, useState } from "react";
2
+ import { approveReport, publishReport, rejectReport } from "../api.js";
3
+ import type { ReportRecord } from "../types.js";
4
+ import "./ReportApprovalPanel.css";
5
+
6
+ interface Props {
7
+ report: ReportRecord;
8
+ onReportChange: (report: ReportRecord) => void;
9
+ }
10
+
11
+ export function ReportApprovalPanel({ report, onReportChange }: Props) {
12
+ const [note, setNote] = useState("");
13
+ const [error, setError] = useState<string | null>(null);
14
+ const [busy, setBusy] = useState(false);
15
+
16
+ const approvalState = report.approvalState ?? "not_required";
17
+ const canApprove = approvalState === "awaiting_approval";
18
+ const canPublish = approvalState === "approved";
19
+
20
+ const history = useMemo(() => [...(report.approvalHistory ?? [])].reverse(), [report.approvalHistory]);
21
+
22
+ async function run(action: "approve" | "reject" | "publish") {
23
+ setBusy(true);
24
+ setError(null);
25
+ try {
26
+ const next = action === "approve"
27
+ ? await approveReport(report.id, note)
28
+ : action === "reject"
29
+ ? await rejectReport(report.id, note)
30
+ : await publishReport(report.id);
31
+ onReportChange(next);
32
+ setNote("");
33
+ } catch (err) {
34
+ setError(err instanceof Error ? err.message : String(err));
35
+ } finally {
36
+ setBusy(false);
37
+ }
38
+ }
39
+
40
+ return <section className="report-approval-panel">
41
+ <div className="report-approval-panel__header">
42
+ <h4>Approval</h4>
43
+ <span className={`card-status-badge card-status-badge--${approvalState}`}>{approvalState}</span>
44
+ </div>
45
+ {canApprove ? <>
46
+ <textarea className="input report-approval-panel__note" value={note} onChange={(event) => setNote(event.target.value)} placeholder="Optional note" />
47
+ <div className="report-approval-panel__actions">
48
+ <button className="btn btn-primary" disabled={busy} onClick={() => run("approve")}>Approve</button>
49
+ <button className="btn btn-danger" disabled={busy} onClick={() => run("reject")}>Reject</button>
50
+ </div>
51
+ </> : null}
52
+ {canPublish ? <div className="report-approval-panel__actions"><button className="btn btn-primary" disabled={busy} onClick={() => run("publish")}>Publish</button></div> : null}
53
+ {error ? <div className="form-error">{error}</div> : null}
54
+ <ul className="report-approval-panel__history">
55
+ {history.map((item, index) => <li key={`${item.decidedAt}-${index}`}>{item.action} by {item.decidedBy} at {item.decidedAt}{item.note ? ` — ${item.note}` : ""}</li>)}
56
+ </ul>
57
+ </section>;
58
+ }
@@ -0,0 +1,21 @@
1
+ import { useMemo } from "react";
2
+ import type { ReportRecord } from "../types.js";
3
+ import { useReportPreview } from "../useReportPreview.js";
4
+ import { useReportSectionDiff } from "../useReportSectionDiff.js";
5
+
6
+ export function ReportComparisonDrawer({ reports, leftId, rightId, onPick, onClose, projectId }: { reports: ReportRecord[]; leftId?: string; rightId?: string; onPick: (slot: "a" | "b", id: string) => void; onClose: () => void; projectId?: string }) {
7
+ const left = useMemo(() => reports.find((r) => r.id === leftId), [reports, leftId]);
8
+ const right = useMemo(() => reports.find((r) => r.id === rightId), [reports, rightId]);
9
+ const leftPreview = useReportPreview(leftId, projectId);
10
+ const rightPreview = useReportPreview(rightId, projectId);
11
+ const diff = useReportSectionDiff(left, right);
12
+ return <div className="modal-overlay open reports-compare-overlay" role="dialog" aria-modal="true" aria-label="Compare reports">
13
+ <div className="modal modal-lg reports-compare">
14
+ <div className="modal-header reports-compare-header"><h3>Compare reports</h3><button className="btn btn-sm" onClick={onClose}>Close</button></div>
15
+ <div className="reports-compare-pickers"><select className="select" value={leftId ?? ""} onChange={(e) => onPick("a", e.target.value)}>{reports.map((r) => <option key={r.id} value={r.id}>{r.title}</option>)}</select>
16
+ <select className="select" value={rightId ?? ""} onChange={(e) => onPick("b", e.target.value)}>{reports.map((r) => <option key={r.id} value={r.id}>{r.title}</option>)}</select></div>
17
+ <div className="reports-compare-frames"><iframe sandbox="allow-same-origin" srcDoc={leftPreview.html} title="Report A" /><iframe sandbox="allow-same-origin" srcDoc={rightPreview.html} title="Report B" /></div>
18
+ <div>Changed: {diff.changed.map((s) => s.id).join(", ")}</div>
19
+ </div>
20
+ </div>;
21
+ }
@@ -0,0 +1,29 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { getReportExportUrl } from "../api.js";
3
+ import { useReportPreview } from "../useReportPreview.js";
4
+ import type { ReportRecord } from "../types.js";
5
+ import { ReportApprovalPanel } from "./ReportApprovalPanel.js";
6
+ import { ShareBlocksPanel } from "./ShareBlocksPanel.js";
7
+
8
+ const SECTION_IDS = ["summary", "system-wins", "system-highlights", "system-lowlights", "system-proposals", "system-deep-dives", "agent-card", "data-coverage", "review-panel"];
9
+
10
+ export function ReportDetailPanel({ report, projectId }: { report?: ReportRecord; projectId?: string }) {
11
+ const frameRef = useRef<HTMLIFrameElement | null>(null);
12
+ const [currentReport, setCurrentReport] = useState<ReportRecord | undefined>(report);
13
+ useEffect(() => setCurrentReport(report), [report]);
14
+ const { html, loading, error } = useReportPreview(currentReport?.id, projectId);
15
+ const sections = useMemo(() => SECTION_IDS, []);
16
+ if (!currentReport) return <div className="reports-detail card">Select a report.</div>;
17
+ return <div className="reports-detail card">
18
+ <div className="reports-detail-header"><h3>{currentReport.title}</h3><a className="btn btn-sm" href={getReportExportUrl(currentReport.id, projectId)} download>Download standalone HTML</a></div>
19
+ <div className="reports-detail-meta">{currentReport.cadence} • {currentReport.status} • {currentReport.periodStart} → {currentReport.periodEnd}</div>
20
+ <div className="reports-detail-body">
21
+ <nav className="reports-detail-sections">{sections.map((section) => <button key={section} className="btn btn-sm" onClick={() => frameRef.current?.contentWindow?.document.querySelector(`[data-section="${section}"]`)?.scrollIntoView()}>{section}</button>)}</nav>
22
+ {loading ? <div>Loading preview...</div> : null}
23
+ {error ? <div>{error}</div> : null}
24
+ <iframe ref={frameRef} sandbox="allow-same-origin" srcDoc={html} title="Report preview" />
25
+ </div>
26
+ <ReportApprovalPanel report={currentReport} onReportChange={setCurrentReport} />
27
+ <ShareBlocksPanel report={currentReport} />
28
+ </div>;
29
+ }
@@ -0,0 +1,3 @@
1
+ export function ReportEmptyState() {
2
+ return <div className="reports-empty card"><h3>No reports found</h3><p>Adjust filters or enable schedules in settings.</p></div>;
3
+ }
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { ReportFilters } from "../types.js";
3
+
4
+ export function ReportFiltersBar({ filters, onChange, agents }: { filters: ReportFilters; onChange: (next: ReportFilters) => void; agents: string[] }) {
5
+ const [query, setQuery] = useState(filters.q);
6
+ useEffect(() => {
7
+ const timeout = setTimeout(() => onChange({ ...filters, q: query }), 250);
8
+ return () => clearTimeout(timeout);
9
+ }, [query]);
10
+
11
+ return <div className="reports-filters">
12
+ <select className="select" value={filters.cadence} onChange={(e) => onChange({ ...filters, cadence: e.target.value as ReportFilters["cadence"] })}><option value="all">All cadence</option><option value="daily">Daily</option><option value="weekly">Weekly</option></select>
13
+ <select className="select" value={filters.status} onChange={(e) => onChange({ ...filters, status: e.target.value as ReportFilters["status"] })}><option value="all">All status</option><option value="generating">Generating</option><option value="review_pending">Review pending</option><option value="review_in_progress">Review in progress</option><option value="review_complete">Review complete</option><option value="approved">Approved</option><option value="published">Published</option><option value="failed">Failed</option></select>
14
+ <input className="input" type="date" value={filters.from} onChange={(e) => onChange({ ...filters, from: e.target.value })} />
15
+ <input className="input" type="date" value={filters.to} onChange={(e) => onChange({ ...filters, to: e.target.value })} />
16
+ <input className="input" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search title" />
17
+ <select className="select" value={filters.agentId} onChange={(e) => onChange({ ...filters, agentId: e.target.value })}><option value="">All agents</option>{agents.map((agent) => <option key={agent} value={agent}>{agent}</option>)}</select>
18
+ </div>;
19
+ }