diff --git a/README.md b/README.md index 8ccb1b5..c6c4408 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,59 @@ # govoplan-campaign -GovOPlaN Campaign module. +GovOPlaN Campaign is the campaign authoring, validation, review, sending-control, and reporting module. It bundles backend campaign APIs with the campaign WebUI package. -This repository owns the Campaign module manifest, backend router, campaign schemas, campaign ORM models, campaign JSON validation, persistence, reports, message building, mock-send, queue/send control services, and campaign/operator/template/address-book WebUI package. The module currently has hard runtime dependencies on `govoplan-files` and `govoplan-mail` because managed attachments and SMTP/IMAP execution are imported directly. +## Ownership -The remaining `app.*` imports are transitional core adapters for auth, audit, Celery dispatch, and legacy access models. +This repository owns: + +- backend module manifest `campaigns` +- campaign, recipient, report, and campaign-audit permissions +- campaign/version/job/issue/send-attempt/append-attempt models and migrations +- campaign JSON schema, validation, message building, attachment resolution, ZIP handling, reports, queue/control services, and mock-send paths +- WebUI package `@govoplan/campaign-webui` +- route contributions for `/campaigns`, `/campaigns/:campaignId/*`, `/operator`, `/reports`, `/address-book`, and `/templates` + +Core owns auth, tenants, RBAC evaluation, database/session primitives, CSRF/API helpers, shell layout, and route rendering. Files and mail own their respective storage and transport capabilities. + +## Dependencies + +The module has runtime dependencies on: + +- `govoplan-core` for platform services +- `govoplan-files` for managed attachment integration +- `govoplan-mail` for SMTP/IMAP profile and delivery integration + +Optional UI behavior can still check installed module availability through core module metadata, but the current backend package declares files and mail as required. ## Development -Install through the core development environment: +Install through the core environment: ```bash cd /mnt/DATA/git/govoplan-core ./.venv/bin/python -m pip install -r requirements-dev.txt ``` -The backend module is registered through the `govoplan.modules` entry point `campaigns`. The frontend package is `@govoplan/campaign-webui`. +Run the WebUI from the core host: + +```bash +cd /mnt/DATA/git/govoplan-core/webui +PATH=/home/zemion/.nvm/versions/node/v22.22.3/bin:$PATH /home/zemion/.nvm/versions/node/v22.22.3/bin/npm run dev +``` + +## Module integration + +Backend entry point: + +```toml +[project.entry-points."govoplan.modules"] +campaigns = "govoplan_campaign.backend.manifest:get_manifest" +``` + +Frontend package: + +```text +@govoplan/campaign-webui +``` + +Platform RBAC and governance rules are documented in `govoplan-core/docs/`. diff --git a/src/govoplan_campaign/backend/manifest.py b/src/govoplan_campaign/backend/manifest.py index a6b4e41..d8f1fd3 100644 --- a/src/govoplan_campaign/backend/manifest.py +++ b/src/govoplan_campaign/backend/manifest.py @@ -114,7 +114,7 @@ manifest = ModuleManifest( route_factory=_campaigns_router, role_templates=ROLE_TEMPLATES, nav_items=( - NavItem(path="/campaigns", label="Campaigns", icon="mail-check", required_any=("campaigns:campaign:read",), order=20), + NavItem(path="/campaigns", label="Campaigns", icon="mail", required_any=("campaigns:campaign:read",), order=20), NavItem( path="/operator", label="Operator Queue", @@ -128,13 +128,13 @@ manifest = ModuleManifest( ), order=30, ), - NavItem(path="/reports", label="Reports", icon="file-text", required_any=("campaigns:report:read",), order=70), + NavItem(path="/reports", label="Reports", icon="reports", required_any=("campaigns:report:read",), order=70), ), frontend=FrontendModule( module_id="campaigns", package_name="@govoplan/campaign-webui", nav_items=( - NavItem(path="/campaigns", label="Campaigns", icon="mail-check", required_any=("campaigns:campaign:read",), order=20), + NavItem(path="/campaigns", label="Campaigns", icon="mail", required_any=("campaigns:campaign:read",), order=20), NavItem( path="/operator", label="Operator Queue", @@ -148,7 +148,7 @@ manifest = ModuleManifest( ), order=30, ), - NavItem(path="/reports", label="Reports", icon="file-text", required_any=("campaigns:report:read",), order=70), + NavItem(path="/reports", label="Reports", icon="reports", required_any=("campaigns:report:read",), order=70), NavItem(path="/address-book", label="Address Book", icon="users", order=80), NavItem(path="/templates", label="Templates", icon="form", order=90), ), diff --git a/webui/src/module.ts b/webui/src/module.ts index f8d12f8..8764655 100644 --- a/webui/src/module.ts +++ b/webui/src/module.ts @@ -1,5 +1,17 @@ import { Activity, FileText, Form, MailCheck, Users } from "lucide-react"; -import type { PlatformWebModule } from "@govoplan/core-webui"; +import { createElement, lazy, useCallback } from "react"; +import { useParams } from "react-router-dom"; +import { Card, ResourceAccessBoundary, type ApiSettings, type AuthInfo, type PlatformWebModule } from "@govoplan/core-webui"; +import { getCampaign } from "./api/campaigns"; + +const AddressBookPage = lazy(() => import("./features/addressbook/AddressBookPage")); +const CampaignListPage = lazy(() => import("./features/campaigns/CampaignListPage")); +const CampaignWorkspace = lazy(() => import("./features/campaigns/CampaignWorkspace")); +const OperatorQueuePage = lazy(() => import("./features/operator/OperatorQueuePage")); +const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage")); + +const campaignRead = ["campaigns:campaign:read"]; +const operatorScopes = ["campaigns:campaign:queue", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:campaign:control", "campaigns:campaign:send"]; export const campaignModule: PlatformWebModule = { id: "campaigns", @@ -8,18 +20,53 @@ export const campaignModule: PlatformWebModule = { dependencies: ["access"], optionalDependencies: ["files", "mail"], navItems: [ - { to: "/campaigns", label: "Campaigns", icon: MailCheck, anyOf: ["campaigns:campaign:read"], order: 20 }, + { to: "/campaigns", label: "Campaigns", icon: MailCheck, anyOf: campaignRead, order: 20 }, { to: "/operator", label: "Operator Queue", icon: Activity, - anyOf: ["campaigns:campaign:queue", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:campaign:control", "campaigns:campaign:send"], + anyOf: operatorScopes, order: 30 }, { to: "/reports", label: "Reports", icon: FileText, anyOf: ["campaigns:report:read"], order: 70 }, { to: "/address-book", label: "Address Book", icon: Users, order: 80 }, { to: "/templates", label: "Templates", icon: Form, order: 90 } + ], + routes: [ + { path: "/campaigns", anyOf: campaignRead, order: 20, render: ({ settings }) => createElement(CampaignListPage, { settings }) }, + { path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 21, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) }, + { path: "/operator", anyOf: operatorScopes, order: 30, render: ({ settings }) => createElement(OperatorQueuePage, { settings }) }, + { path: "/reports", anyOf: ["campaigns:report:read"], order: 70, render: () => createElement(ReportsPage) }, + { path: "/address-book", order: 80, render: () => createElement(AddressBookPage) }, + { path: "/templates", order: 90, render: () => createElement(TemplatesPage) } ] }; +function CampaignResourceRoute({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) { + const { campaignId = "" } = useParams(); + const tenantId = (auth.active_tenant ?? auth.tenant).id; + const probe = useCallback(() => getCampaign(settings, campaignId), [settings.apiBaseUrl, settings.apiKey, settings.accessToken, campaignId, tenantId]); + + return createElement(ResourceAccessBoundary, { + probe, + resetKey: `${tenantId}:${campaignId}`, + fallback: "/campaigns", + children: createElement(CampaignWorkspace, { settings, auth }) + }); +} + +function ReportsPage() { + return createElement( + "div", + { className: "content-pad" }, + createElement( + "div", + { className: "page-heading" }, + createElement("h1", null, "Reports"), + createElement("p", null, "This module is prepared but not implemented yet.") + ), + createElement(Card, null, createElement("p", { className: "muted" }, "Next passes will add functionality here.")) + ); +} + export default campaignModule;