@runfusion/fusion 0.25.0 → 0.27.0
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/README.md +6 -0
- package/dist/bin.js +28004 -16888
- package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
- package/dist/client/assets/AgentDetailView-DwLmRXTY.js +18 -0
- package/dist/client/assets/{AgentsView-B3jYk8Kt.js → AgentsView-D-N6aA0P.js} +12 -7
- package/dist/client/assets/ChatView-DnCdKu8Z.js +1 -0
- package/dist/client/assets/{DevServerView-DyGDEiBP.js → DevServerView-BiA1nYtt.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-D5UIeIl6.js → DirectoryPicker-DvBviDG6.js} +1 -1
- package/dist/client/assets/{DocumentsView-DNHu1T8K.js → DocumentsView-BWXOxpuq.js} +1 -1
- package/dist/client/assets/{EvalsView-CpRobtDi.js → EvalsView-CJFbtL7i.js} +1 -1
- package/dist/client/assets/{ExperimentalAgentOnboardingModal-DOY_oZi7.js → ExperimentalAgentOnboardingModal-DuGIPd0B.js} +1 -1
- package/dist/client/assets/InsightsView-BBpRiolN.js +11 -0
- package/dist/client/assets/{MemoryView-PSc5lGJt.js → MemoryView-48LuNkKk.js} +2 -2
- package/dist/client/assets/NodesView-CGQWSNZM.js +14 -0
- package/dist/client/assets/{PiExtensionsManager-DL_QcN56.js → PiExtensionsManager-i-7UL2oh.js} +2 -2
- package/dist/client/assets/PluginManager-DoSAykD6.js +1 -0
- package/dist/client/assets/{ResearchView-BzCcDAS4.css → ResearchView-BEI4ZSGs.css} +1 -1
- package/dist/client/assets/ResearchView-XZuRtOxE.js +1 -0
- package/dist/client/assets/SettingsModal-Ci0_sqbU.css +1 -0
- package/dist/client/assets/{SettingsModal-CUCyaAyE.js → SettingsModal-CmeF8CN4.js} +1 -1
- package/dist/client/assets/SettingsModal-DBcjf9Bu.js +31 -0
- package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
- package/dist/client/assets/{SetupWizardModal-BKscasuh.js → SetupWizardModal-CgtvpMX9.js} +1 -1
- package/dist/client/assets/{SkillsView-BdELqTy7.js → SkillsView-DErYRumF.js} +1 -1
- package/dist/client/assets/StashRecoveryView-B_8WIQEo.css +1 -0
- package/dist/client/assets/StashRecoveryView-QJrNS4Vg.js +1 -0
- package/dist/client/assets/{TodoView-DFNGBDNV.js → TodoView-BD9NRwq0.js} +2 -2
- package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
- package/dist/client/assets/dashboard-view-BWGH_fAq.js +63 -0
- package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
- package/dist/client/assets/dashboard-view-DdGlfuu-.css +1 -0
- package/dist/client/assets/dashboard-view-Ws9_ZnKu.js +21 -0
- package/dist/client/assets/{folder-open-k1xmUMyr.js → folder-open-CHSlllzf.js} +1 -1
- package/dist/client/assets/index-DCovGm5b.css +1 -0
- package/dist/client/assets/index-bEwSVl7B.js +692 -0
- package/dist/client/assets/{star-ne32r3Y4.js → star-BgVwWAPz.js} +1 -1
- package/dist/client/assets/{upload-MS-2Gx53.js → upload-CAzycxr9.js} +1 -1
- package/dist/client/assets/{users-C519GSjH.js → users-CZnxCCCJ.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
- package/dist/extension.js +15810 -10205
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
- package/dist/pi-claude-cli/src/provider.ts +7 -1
- package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +24 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/package.json +44 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +36 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +58 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
- package/dist/plugins/fusion-plugin-reports/package.json +18 -2
- package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
- package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
- package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
- package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
- package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
- package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
- package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
- package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
- package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
- package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
- package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
- package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
- package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
- package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
- package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
- package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
- package/dist/plugins/fusion-plugin-roadmap/bundled.js +1672 -0
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
- package/dist/plugins/fusion-plugin-roadmap/package.json +4 -41
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
- package/package.json +2 -3
- package/skill/fusion/references/engine-tools.md +1 -1
- package/skill/fusion/references/extension-tools.md +3 -3
- package/skill/fusion/references/fusion-capabilities.md +1 -1
- package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
- package/dist/client/assets/AgentDetailView-ZbHEbYRT.js +0 -18
- package/dist/client/assets/ChatView-DhPkiEGs.js +0 -1
- package/dist/client/assets/InsightsView-vp0RE8Mg.js +0 -11
- package/dist/client/assets/NodesView-DMj6HGeC.js +0 -14
- package/dist/client/assets/PluginManager-BtYKm8IT.js +0 -1
- package/dist/client/assets/ResearchView-BhWqfdV0.js +0 -1
- package/dist/client/assets/SettingsModal-BAgB4_AR.js +0 -31
- package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
- package/dist/client/assets/SettingsModal-DzsLquBu.css +0 -1
- package/dist/client/assets/index-Qq2JOOWx.css +0 -1
- package/dist/client/assets/index-TFYXEVpn.js +0 -692
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +0 -101
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +0 -92
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +0 -48
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +0 -31
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +0 -1299
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +0 -2559
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +0 -1144
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +0 -1756
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +0 -70
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +0 -7
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +0 -8
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +0 -1188
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +0 -20
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +0 -6
- package/dist/plugins/fusion-plugin-roadmap/src/index.ts +0 -74
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +0 -41
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +0 -15
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +0 -15
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +0 -283
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +0 -21
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +0 -310
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +0 -5
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +0 -361
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +0 -408
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +0 -68
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +0 -300
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +0 -381
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +0 -3
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +0 -445
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +0 -334
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +0 -1318
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +0 -163
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +0 -37
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +0 -188
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +0 -311
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +0 -299
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +0 -765
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +0 -1001
|
@@ -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
|
+
}
|
package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx
ADDED
|
@@ -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
|
+
});
|
package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx
ADDED
|
@@ -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,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
|
+
}
|