feat(FN-3787): add approval workflow and share blocks to reports plugin

Added an approval workflow to the reports plugin comprising a state machine (`approval.ts`), share blocks logic (`share-blocks.ts`), API routes for approvals, and two new dashboard panels (ReportApprovalPanel and ShareBlocksPanel), with corresponding tests; also updated the plugin README and added a

Fusion-Task-Id: FN-3787
This commit is contained in:
Fusion
2026-05-10 14:06:49 -07:00
committed by gsxdsm
parent 1209e6b452
commit 63fe25e014
23 changed files with 1063 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import { EventEmitter } from "node:events";
import type { Database } from "@fusion/core";
import type { ApprovalDecision, ApprovalState } from "../approval.js";
import type { CombinedReview } from "../review-types.js";
import {
type Report,
@@ -27,6 +28,8 @@ interface ReportRow {
publishedAt: string | null;
archivedAt: string | null;
failureReason: string | null;
approval_state: ApprovalState;
approval_history: string;
draftMarkdown: string | null;
renderedHtmlPath: string | null;
rendered_html: string | null;
@@ -76,6 +79,8 @@ export class ReportStore extends EventEmitter<ReportStoreEvents> {
publishedAt: null,
archivedAt: null,
failureReason: null,
approvalState: "not_required",
approvalHistory: [],
draftMarkdown: input.draftMarkdown ?? null,
renderedHtmlPath: null,
renderedHtml: null,
@@ -92,11 +97,13 @@ export class ReportStore extends EventEmitter<ReportStoreEvents> {
id, cadence, periodStart, periodEnd, title, status,
generationStartedAt, generationCompletedAt, reviewStartedAt, reviewCompletedAt,
approvedAt, approvedBy, publishedAt, archivedAt, failureReason,
approval_state, approval_history,
draftMarkdown, renderedHtmlPath, rendered_html, rendered_html_generated_at, metadataJson, combinedReviewJson, createdAt, updatedAt
) VALUES (
@id, @cadence, @periodStart, @periodEnd, @title, @status,
@generationStartedAt, @generationCompletedAt, @reviewStartedAt, @reviewCompletedAt,
@approvedAt, @approvedBy, @publishedAt, @archivedAt, @failureReason,
@approvalState, @approvalHistory,
@draftMarkdown, @renderedHtmlPath, @renderedHtml, @renderedHtmlGeneratedAt, @metadataJson, @combinedReviewJson, @createdAt, @updatedAt
)
`).run(this.toDbParams(report, true));
@@ -164,6 +171,13 @@ export class ReportStore extends EventEmitter<ReportStoreEvents> {
renderedHtml: patch.renderedHtml ?? current.renderedHtml,
renderedHtmlGeneratedAt: patch.renderedHtmlGeneratedAt ?? current.renderedHtmlGeneratedAt,
failureReason: patch.failureReason ?? current.failureReason,
approvalState: patch.approvalState ?? current.approvalState,
approvalHistory: patch.approvalHistory ?? current.approvalHistory,
status: patch.status ?? current.status,
approvedAt: patch.approvedAt ?? current.approvedAt,
approvedBy: patch.approvedBy ?? current.approvedBy,
publishedAt: patch.publishedAt ?? current.publishedAt,
reviewCompletedAt: patch.reviewCompletedAt ?? current.reviewCompletedAt,
updatedAt: new Date().toISOString(),
};
@@ -269,6 +283,8 @@ export class ReportStore extends EventEmitter<ReportStoreEvents> {
publishedAt: row.publishedAt,
archivedAt: row.archivedAt,
failureReason: row.failureReason,
approvalState: row.approval_state,
approvalHistory: this.parseApprovalHistory(row.approval_history),
draftMarkdown: row.draftMarkdown,
renderedHtmlPath: row.renderedHtmlPath,
renderedHtml: row.rendered_html,
@@ -297,6 +313,8 @@ export class ReportStore extends EventEmitter<ReportStoreEvents> {
publishedAt = @publishedAt,
archivedAt = @archivedAt,
failureReason = @failureReason,
approval_state = @approvalState,
approval_history = @approvalHistory,
draftMarkdown = @draftMarkdown,
renderedHtmlPath = @renderedHtmlPath,
rendered_html = @renderedHtml,
@@ -329,6 +347,8 @@ export class ReportStore extends EventEmitter<ReportStoreEvents> {
publishedAt: report.publishedAt,
archivedAt: report.archivedAt,
failureReason: report.failureReason,
approvalState: report.approvalState,
approvalHistory: JSON.stringify(report.approvalHistory ?? []),
draftMarkdown: report.draftMarkdown,
renderedHtmlPath: report.renderedHtmlPath,
renderedHtml: report.renderedHtml,
@@ -357,4 +377,14 @@ export class ReportStore extends EventEmitter<ReportStoreEvents> {
return null;
}
}
private parseApprovalHistory(json: string | null): ApprovalDecision[] {
if (!json) return [];
try {
const parsed = JSON.parse(json);
return Array.isArray(parsed) ? parsed as ApprovalDecision[] : [];
} catch {
return [];
}
}
}

View File

@@ -1,3 +1,4 @@
import type { ApprovalDecision, ApprovalState } from "../approval.js";
import type { CombinedReview } from "../review-types.js";
export type ReportCadence = "daily" | "weekly" | "monthly" | "quarterly" | "manual";
@@ -28,6 +29,8 @@ export interface Report {
publishedAt: string | null;
archivedAt: string | null;
failureReason: string | null;
approvalState: ApprovalState;
approvalHistory: ApprovalDecision[];
draftMarkdown: string | null;
renderedHtmlPath: string | null;
renderedHtml: string | null;
@@ -47,7 +50,7 @@ export interface ReportCreateInput {
draftMarkdown?: string;
}
export type ReportUpdateInput = Partial<Pick<Report, "title" | "draftMarkdown" | "renderedHtmlPath" | "renderedHtml" | "renderedHtmlGeneratedAt" | "metadata" | "failureReason">>;
export type ReportUpdateInput = Partial<Pick<Report, "title" | "draftMarkdown" | "renderedHtmlPath" | "renderedHtml" | "renderedHtmlGeneratedAt" | "metadata" | "failureReason" | "approvalState" | "approvalHistory" | "status" | "approvedAt" | "approvedBy" | "publishedAt" | "reviewCompletedAt">>;
export interface ReportListFilter {
cadence?: ReportCadence;