The highlighted box added in fa86fc5 made the URL prominent and
copyable, and an operator promptly copied it and opened it in a browser
tab — a reasonable reading of "here is a URL, copy it" that lands on a
page which cannot do anything without a login attached.
Reworded to lead with the verb that matters ("paste this into ... then
save it there") and added the missing half in plain words: don't open it
yourself, it is where LinkedIn sends you back after login. The backend
callback now explains the same thing for anyone who opens it anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confirmed the architecture is correct as-is: each company already has
its own isolated envs.json, providers.json and LinkedIn OAuth tokens,
swapped in on switch (company_profiles.py) — LinkedIn's own client_id and
secret are meant to be per-company, by design, and nothing about that
changes here.
What was genuinely broken: the exact URL a new company's owner has to add
to their LinkedIn app before Authenticate can work was rendered as small
gray footnote text below the status badges, easy to open LinkedIn
Developer Portal without ever noticing. That's the entire reason a
working setup on one machine is completely unreproducible from memory a
week later on another one.
Moved it to a highlighted box, first thing shown in the card, with the
warning stated in plain words and a one-click Copy button — removing the
one place a manual retype silently drops a character. No backend change:
the URL itself was already computed correctly and consistently; only
where and how it's shown was the problem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found the real cause of the cross-laptop Gemini failure. The backend
fallback fix (876b315) never actually ran for this button: pressing "Test
Connection" with Vertex AI enabled always called testModelConnection with
one specific model_id, never the provider-level test that has the
fallback. That model_id was the currently active model if one was already
chosen — or, on a brand-new setup with nothing chosen yet,
provider.models[0], which is always GEMINI_MODELS[0]: gemini-3.6-flash,
the newest registry entry.
Vertex AI Model Garden availability is rolled out per Google Cloud
project. Two laptops with an identical API key and an identical, valid
Project ID produced opposite results because one had an older model
already saved as active (tested that, worked) and the other had nothing
saved yet (defaulted to the newest model, which this project's Vertex
console had not yet been granted, and failed outright).
Now: a chosen model is still tested exactly as itself — this button's job
is to confirm the model actually in use still works, and silently
swapping it for a different one would hide a real break. With nothing
chosen yet, it calls testProviderConnection instead of guessing, which
tries every known model and succeeds on the first that works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
package-lock.json had drifted from node_modules — mermaid resolved to
11.16.1 against the unchanged "^11.6.0" range in package.json (was
11.12.3 in the committed lockfile), pulling in that release's own
dependency changes (chevrotain/langium/vscode-languageserver-* dropped,
stream-parser/probe-image-size/es-toolkit/@upsetjs/venn.js added — all
internal to mermaid's diagram parsing, nothing added at the top level).
Leaving this uncommitted would mean a fresh `npm install` on another
machine installs older versions than the ones already running here —
exactly the cross-machine mismatch this push is meant to prevent. Build
and lint both verified clean against the versions this lockfile now
records.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cropped from the real Maskan Technologies mark (logo/maskan_logo.png) —
just the gradient shape, not the wordmark, since Meta displays this icon
small and often circular. Upscaled from a 157x136 source with LANCZOS;
clean at this size because it's a flat-gradient shape with no fine detail
to blur, but a native high-res export should replace it when one exists.
Committed so it isn't only a local file on one machine — it's the actual
asset submitted (or about to be) to developers.facebook.com for app
1592299622228272.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every lint run this session reported the same 7 warnings across 4 files.
Fixed properly, not suppressed:
- Chat/index.tsx: effect depended on the whole liveChatStatus object;
narrowed to liveChatStatus?.requestId, the one field it actually reads.
- Heartbeat/index.tsx: fetchConfig wrapped in useCallback([form, t]) and
added to its effect's dependency array, instead of an empty array
silencing the warning on a function that closes over both.
- Environments/index.tsx: `t` (i18n) added to 4 useCallback dependency
arrays that read it but didn't declare it.
- ModelsSection.tsx: effect depended on the currentSlot object reference;
extracted currentProviderId/currentModel primitives so it only reruns
when the actual values change, not on every new object identity.
Build 0 errors, lint 0 errors / 0 warnings (was 7).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Balance: INR 0.00" was technically accurate and completely misleading —
that field means amount owed, not money on deposit, and it is 0 by design
on a prepay account right after funds are added. A prepay account now
shows "Available funds" using Meta's own display string instead
(e.g. "Available balance (₹50.00 INR)"); a post-pay account is unaffected
and still shows Balance as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It falls back to the backend's own configured account when the env var is
unset, which is the default case — no .env ships with the repo. Previously
the Ad account card, and with it balance, spend and minimum daily budget,
just did not appear, with nothing explaining why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The button was disabled, which meant a campaign left over from a failed
connection test could not be cleared from MaskanX at all.
It is enabled now and sends delete_in_meta=true, behind a confirmation
that says plainly the campaign was not created here and that deleting it
removes it from Meta permanently — and tells you to close the dialog if it
is real Ads Manager work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the backend split between "can launch" and "can run a connection
test". An account with no payment method still gets the test, because the
test only creates paused objects — and that is the case where confirming
Meta accepts our requests matters most.
The disabled note now names the check that is actually in the way instead
of implying every failed check is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>