The Campaigns page now opens with a Setup panel listing every check that
can stop a launch — token, ad account, account status, payment method,
Page, operator identity — each failure carrying the sentence that says
what to do about it. A red cross on its own tells the operator nothing
they can act on.
When a Page id is missing, the backend names the Pages the token can
already reach, which turns "set an id" into "use this one".
Once the checks pass, Run connection test builds a real campaign chain in
Meta, confirms it is paused, and deletes it. It sits behind a confirmation
that says plainly what will appear in the ad account and that nothing can
be spent.
VITE_META_AD_ACCOUNT_ID is normalised to undefined when empty so the
backend falls back to META_ADS_ACCOUNT_ID rather than being asked about an
empty account id.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Yesterday and last-7-days panels, the most and least efficient campaign by
cost per lead, and a per-campaign table.
Both windows end yesterday, matching the API: today's figures are partial
and would make every morning read as a collapse in spend.
Costs render as a dash rather than zero when there is nothing to divide
by. A campaign with no leads yet has no cost per lead, and zero would read
as "free".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A campaign that stopped overnight has to be explainable at a glance, so
the rule and the observed value ride on the status tag itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Status-aware row actions, an operator token field, sync failure detail on
hover, and a delete action whose confirmation says what is actually about
to be destroyed.
The dialog says what will actually be destroyed, which differs by case:
a synced MaskanX campaign is deleted on Meta too, an imported one is only
forgotten here, and a draft never reached Meta at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Row actions are status-aware: Sync only on approved, Launch only on synced
or paused, Pause only on live. Imported campaigns offer no Sync, since they
were authored in Ads Manager.
Launch is the only action behind a confirmation, and the dialog names the
daily budget and says spending begins immediately. It is the last human
checkpoint before money moves.
The operator token is stored in localStorage and sent as X-MaskanX-Operator
on approve, sync, launch, pause and stop. The backend resolves the operator
name from it and ignores any actor in the body, so approval stays
attributable; without a token, launch is refused and the panel says so.
A failed sync shows its stored Meta error on hover rather than a bare status.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaigns page with ad-account billing panel, six-step creation wizard,
and Meta ad preview across six placements.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Form.useWatch([], form) tracks getFieldsValue(), which returns {} on a step
with no mounted Form.Item. The Preview and Review steps have none, so on
entering Preview the creative memo recomputed to empty and previewed a blank
ad pointing at example.com, and the Review step rendered "-" for every field.
The submitted payload was always correct — submit() reads the preserved store
via getFieldsValue(true) — so this was a display and preview defect, not data
loss. Pass preserve: true so the watch tracks the whole store.
Also document VITE_META_AD_ACCOUNT_ID. Unset, it silently disables the
billing panel, the missing-payment-method warning, ad preview, and the
account-minimum budget check, with no diagnostic anywhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Critical: only the active wizard step is mounted, so on the Review step no
Form.Items are registered and form.validateFields() resolved to {}. Every
field was dropped and every submit failed validation server-side. Read the
preserved store with getFieldsValue(true) instead, and validate each step's
fields when leaving it so the required rules actually run.
Security: extractSrc matched src=" anywhere in the body with no scheme or
host check, so a data-src attribute or an earlier <img> could hijack the
match, and javascript:/data:/protocol-relative URLs passed through. Combined
with sandbox="allow-same-origin" a javascript: URL would have inherited the
app origin. It now anchors on the iframe tag and accepts only https URLs on
facebook.com/fbcdn.net, failing closed otherwise. Verified against 14 inputs
including both hijack shapes.
Also: memoise the creative object so PreviewStep does not refetch on every
parent render, type the form values, guard the preview effect against stale
responses, reset the wizard on close, and show an explicit message when no
placement renders.
Moving extractSrc to its own module removes the react-refresh lint
suppression rather than hiding it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PUT /campaigns/{id} is a partial update on the backend, applying only the
fields present in the body. Requiring budget and guardrails in the client
type forced callers to resend both just to change a name. `name` stays
required because the backend request model requires it on POST and PUT.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the typed API client for campaign management (Task 7): campaign.ts
types, campaignApi module wired to the backend's /campaigns endpoints,
and registration in the aggregated api index. This is the API layer
only; the list page and wizard land in Tasks 8/9.
Independent React, TypeScript and Vite web application for MaskanX.
Includes the chat console, agent and persona configuration, MCP client
management, model and provider settings, cron jobs, sessions, and
diagnostics.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>