Close Campaign interface audit gaps

This commit is contained in:
2026-08-04 01:04:39 +02:00
parent 3f3545f080
commit df5a93d6a3
10 changed files with 153 additions and 15 deletions
@@ -5,6 +5,7 @@ import { DismissibleAlert } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
import { LoadingFrame } from "@govoplan/core-webui";
import { ActionBlockerHint, DocumentationHelpLink } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
export default function CampaignAuditPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
@@ -27,7 +28,27 @@ export default function CampaignAuditPage({ settings, campaignId }: {settings: A
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_audit_data.af52b968">
<Card title="i18n:govoplan-campaign.recent_audit_events.7ec32b1d">
<p className="muted">i18n:govoplan-campaign.campaign_specific_audit_api_integration_will_be_.e53c8280</p>
<ActionBlockerHint
tone="info"
reason={{
summary: "Campaign-specific audit projection is not available on this page.",
details: "Campaign actions already emit bounded platform audit evidence. Authorized readers can inspect it in the Audit administration surface.",
requiredAction: "Open tenant audit and filter by the campaign identifier.",
actor: "Audit reader or system operator",
target: "Administration > Tenant audit"
}}
documentation={{
topicId: "campaigns.reference.composition-assurance",
documentationType: "admin"
}}
/>
<DocumentationHelpLink
reference={{
topicId: "campaigns.reference.composition-assurance",
documentationType: "user"
}}
label="Open Campaign assurance documentation"
/>
</Card>
</LoadingFrame>
</div>);
@@ -29,6 +29,11 @@ export default function CampaignJsonView({ settings, campaignId }: {settings: Ap
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
<DismissibleAlert tone="warning" dismissible={false}>
This expert view contains the complete authorized campaign configuration,
including recipient and message fields that may contain personal data.
Download and share it only for an authorized purpose.
</DismissibleAlert>
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_json.812c7a50">
<Card>
{!loading || version ? <pre className="code-panel">{JSON.stringify(campaignJson, null, 2)}</pre> : <pre className="code-panel">{"{}"}</pre>}
@@ -1,6 +1,6 @@
import { useState } from "react";
import { Link2, X } from "lucide-react";
import { Button, Dialog, i18nMessage } from "@govoplan/core-webui";
import { Button, Dialog, DismissibleAlert, i18nMessage } from "@govoplan/core-webui";
import type {
CampaignAttachmentPreviewFile,
@@ -108,7 +108,11 @@ export default function AttachmentLinkingPreview({
value={loading ? "..." : preview?.shared_file_count ?? "—"}
/>
</div>
{error && <p className="review-flow-inline-note is-danger">{error}</p>}
{error && (
<DismissibleAlert tone="danger" compact dismissible={false}>
{error}
</DismissibleAlert>
)}
{!error && unlinkedCount > 0 && (
<p className="review-flow-inline-note is-stale">
i18n:govoplan-campaign.unlinked_candidate_files_are_not_yet_part_of_the.b8fd5998
+2 -5
View File
@@ -17,7 +17,6 @@ import "./styles/campaign-workspace.css";
const CampaignModulePage = lazy(() => import("./features/campaigns/CampaignModulePage"));
const CampaignWorkspace = lazy(() => import("./features/campaigns/CampaignWorkspace"));
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
const campaignRead = ["campaigns:campaign:read"];
const reportRead = ["campaigns:report:read"];
@@ -91,16 +90,14 @@ export const campaignModule: PlatformWebModule = {
}
],
navItems: [
{ to: "/campaigns", label: "i18n:govoplan-campaign.campaigns.01a23a28", iconName: "campaign", anyOf: campaignModuleRead, order: 20 },
{ to: "/templates", label: "i18n:govoplan-campaign.templates.f25b700e", iconName: "layout-template", order: 90 }],
{ to: "/campaigns", label: "i18n:govoplan-campaign.campaigns.01a23a28", iconName: "campaign", anyOf: campaignModuleRead, order: 20 }],
routes: [
{ path: "/campaigns", anyOf: campaignModuleRead, order: 20, render: ({ settings, auth }) => createElement(CampaignModuleLandingRoute, { settings, auth }) },
{ path: "/operator", anyOf: OPERATOR_QUEUE_ROUTE_SCOPES, allOf: campaignRead, order: 21, surfaceId: operatorQueueSurface, render: () => createElement(Navigate, { to: "/campaigns/queue", replace: true }) },
{ path: "/campaigns/queue", anyOf: OPERATOR_QUEUE_ROUTE_SCOPES, allOf: campaignRead, order: 21, surfaceId: operatorQueueSurface, render: ({ settings, auth }) => createElement(CampaignModulePage, { active: "queue", settings, auth }) },
{ path: "/campaigns/reports", anyOf: reportRead, order: 22, surfaceId: reportsSurface, render: ({ settings, auth }) => createElement(CampaignModulePage, { active: "reports", settings, auth }) },
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 22, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) },
{ path: "/templates", order: 90, render: () => createElement(TemplatesPage) }],
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 22, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) }],
uiCapabilities: {
"dashboard.widgets": campaignDashboardWidgets,
"wizard.directories": campaignWizardDirectories
@@ -32,6 +32,47 @@ const preview = fs.readFileSync(
path.join(sourceRoot, "features/campaigns/components/MessagePreviewOverlay.tsx"),
"utf8",
);
const styles = fs.readFileSync(
path.join(sourceRoot, "styles/campaign-workspace.css"),
"utf8",
);
const jsonView = fs.readFileSync(
path.join(sourceRoot, "features/campaigns/CampaignJsonView.tsx"),
"utf8",
);
const auditView = fs.readFileSync(
path.join(sourceRoot, "features/campaigns/CampaignAuditPage.tsx"),
"utf8",
);
const attachmentPreview = fs.readFileSync(
path.join(sourceRoot, "features/campaigns/review/AttachmentLinkingPreview.tsx"),
"utf8",
);
assert.ok(
jsonView.includes('DismissibleAlert tone="warning" dismissible={false}'),
"The full Campaign JSON projection must retain an explicit privacy warning",
);
assert.ok(
auditView.includes("ActionBlockerHint"),
"Unavailable Campaign audit projection must retain an actionable shared blocker",
);
assert.ok(
attachmentPreview.includes('<DismissibleAlert tone="danger" compact dismissible={false}>'),
"Attachment-preview validation failures must use the compact shared alert",
);
assert.ok(
styles.includes("@media (prefers-reduced-motion: reduce)"),
"Campaign must retain an explicit reduced-motion presentation contract",
);
assert.ok(
styles.includes("@media (max-width:"),
"Campaign must retain responsive narrow-viewport layouts",
);
assert.ok(
styles.includes(":focus-visible"),
"Campaign-specific interactive controls must retain visible keyboard focus",
);
for (const handler of ["onFirst", "onPrevious", "onNext", "onLast"]) {
const buttonLine = preview
.split("\n")