# Work tracker — 1 October 2026

Local continuity reference for campaign creation, platform connections, and location targeting. This file is ignored by Git. Implementation, database checks, automated tests, and live provider validation are recorded separately. The README contains the architecture and deployment commands.

## Completed work

### Ad preview identity

**Description:** The preview uses the selected Facebook Page and Instagram identity instead of the literal “Ad Page” placeholder. Saved names survive reloads, older Page selections resolve, and Instagram-only placement is supported. Tests cover identity changes and HTML escaping.

### Campaign and creative names

**Description:** Default campaign names include the creator, platform, and date. Default creative names include concept, format, and date. User-entered names remain unchanged.

### Country picker and catalog

**Description:** The six hardcoded picker options were replaced with a searchable local catalog of 249 ISO countries and territories in config/countries.php. Priority markets begin with US, GB, IN, CA, AU, DE, FR, ES, IT, NL, BR, MX, JP, SG, IE, NZ, CH, and SE. The catalog is in the working tree but is not committed.

### Country storage and provider mappings

**Description:** Added locations, platform_locations, and a repeatable CountryLocationSeeder. Meta, Google, and LinkedIn lookup stores verified provider IDs. Publishing rejects any country that cannot be resolved exactly, preventing a failed lookup from broadening the audience. Hardcoded Google and LinkedIn publisher country maps were removed.

**Development database:** Migrations and seeding ran successfully. The user confirmed 249 country rows with priority countries first by ascending ID. Live read-only lookups for US, GB, IN, and CA returned exact matches on all three platforms; those 12 mappings were saved as verified. Queries still require ORDER BY id to guarantee display order.

### State, city, and DMA source decision

**Description:** Reviewed the older AdCenter project's country, US state/city, and DMA sources. This app does not read AdCenter's database during requests. No bulk state, city, or DMA seeder was added; cities and regions are discovered through the platforms, with selected provider IDs stored locally.

### Platform connection credentials

**Description:** Readable connection identifiers remain in platform_data; platform_secrets, access tokens, and refresh tokens are encrypted. Meta, Google, and LinkedIn runtime connections prefer stored credentials, then configuration/.env fallback. Seeding fills missing fields without replacing stored tokens. Temporary hardcoded fallbacks remain until a connection-management UI exists.

**Caution:** Setting is_active to true cannot repair ciphertext written under a different APP_KEY. The original key or a new connection is required. Keep token values out of this tracker.

### Targeting persistence and backfill — Phase 1

**Description:** Added versioned targeting_sets and targeting_locations, compatibility handling for legacy country JSON, revision checks, and repeatable targeting:backfill. The development database was migrated and existing country selections backfilled while retaining legacy campaign JSON.

### Draft location selection — Phase 2

**Description:** Added LocationDiscovery and a chat location chooser for Meta and Google draft campaigns. Search is country-scoped, displays full labels and provider IDs, verifies the selected ID again before saving, and supports listing and removing selections. Meta offers up to four pages of 25 results; Google offers the first 25 and supports City, State/Province refinement.

### Draft location publishing — Phase 2

**Description:** Meta receives verified city/region keys; Google receives geoTargetConstants resource names. A selected city or region narrows its country while other selected countries remain. Stale, mismatched, or unsupported mappings block publication. Approval checks include targeting revisions, and the review panel shows selected places.

### Imported campaign geography — Phase 2

**Description:** Meta import retains the complete returned geo_locations payload. Google import accepts an explicit campaign ID from the allowed customer account and retains included/excluded criterion IDs and labels. Incomplete Google responses and proximity/location-group criteria abort import. Unrelated edits preserve imported geography; republishing cannot silently discard raw criteria.

### Imported Google budget edits — Phase 2

**Description:** Budget edits require a known budget ID, reject shared or unknown ownership, and read current provider state before changing it. Edit approval rejects changes made after review. Published location edits remain blocked until a verified in-place update path exists.

## Verification and working-tree notes

### Automated checks

**Description:** The last affected cross-platform run passed 168 tests and 620 assertions. Pint and git diff --check passed after code changes. Provider calls in these tests were faked; the tests do not prove live Meta or Google payload acceptance.

### Branch review

**Description:** Campaign, targeting, connection, migration, factory, and test changes remain in the working tree and have not been committed or deployed. The country picker has a browser-level test in tests/js/choice-boxes.cjs. Connection changes also affect Google OAuth and keyword research, LinkedIn account and organization resolution, and token refreshers; include these in regression review.

### Local permission script

**Description:** pf.sh has separate uncommitted permission changes. Its current find commands may grant other users read access to .env and .env.bak if run on a server. Review and restrict it before running it. The local .env.bak file was changed to owner-only permissions and is ignored by Git; its contents were not recorded here.

### Tracker visibility

**Description:** The entire docs/ directory is ignored by Git. This file is a local continuity note and does not appear in normal Git status.

## Open chat and publishing review findings

These findings are **not fixed**. Resolve the P1 items before releasing chat-driven campaign mutations. The findings concern authorization, approval enforcement, campaign identity, and duplicate publication; the completed targeting work above does not address them.

### P1 — Authenticate and authorize the LinkedIn MCP endpoint

**Description:** [routes/ai.php](../routes/ai.php) registers /mcp/linkedin without authentication middleware. A reported unauthenticated request reached linkedin_update_entity and returned HTTP 200 with the platform service mocked. With valid provider credentials and an allowed account, the endpoint could modify campaigns.

**To do:** Require authentication at the route and enforce authorization for the requested account and entity on every mutating tool. Add an unauthenticated and unauthorized regression test.

### P1 — Enforce approvals in Google chat mutations

**Description:** [GoogleAdsAgent.php](../app/Agent/GoogleAdsAgent.php) exposes create and update tools through wrappers that do not enforce the main chat's approval gate. An update can request status=enabled or change a budget directly; model instructions to confirm are not an enforcement boundary.

**To do:** Route Google creation and mutation tools through an enforced approval mechanism and test both approval and rejection before any provider write.

### P1 — Bind approval to the reviewed campaign and state

**Description:** [PublishCampaign.php](../app/Agent/Tools/Campaign/PublishCampaign.php) resolves the campaign from current workspace focus when an approved tool call executes. The pending call has no campaign ID or reviewed snapshot. A focus switch while publication is deferred can make approval act on another campaign. Activation and pushing changes use the same focus-dependent pattern.

**To do:** Bind each approval to a campaign ID and a reviewed state or revision; recheck both when it executes. Reject stale or mismatched approvals, including focus-switching calls in the same batch.

### P1 — Prevent concurrent duplicate publication

**Description:** [Publisher.php](../app/Campaigns/Publisher.php) checks external_campaign_id before the remote call, but has no atomic claim or lock. Two concurrent requests can both pass and create remote campaigns; the later database update can overwrite the first set of external IDs.

**To do:** Claim publication atomically before calling the provider, handle retries and failed attempts, and test overlapping requests.

### P1 — Keep the selected Google account per conversation and campaign

**Description:** [SelectAccount.php](../app/Mcp/Tools/GoogleAds/SelectAccount.php) changes the user's shared GoogleAdsConnection customer_id. Selecting account B in another chat can redirect an account-A draft or break an update to an existing campaign.

**To do:** Persist the chosen customer ID on the conversation and campaign, use it for all later reads and writes, and recheck account access at mutation time. Test two chats selecting different accounts.

### P2 — Submit all pending approval decisions together

**Description:** [ChatController.php](../app/Http/Controllers/ChatController.php) sends one decision per request. The installed SDK requires a decision for every pending approval in the batch; a reported two-call batch produced ApprovalMismatchException. The error response also clears pending controls.

**To do:** Submit the complete decision batch and retain actionable approval controls when resolution fails. Test mixed approve/reject choices and retry after an error.

### Review validation

**Description:** The reported initial targeted run passed 255 of 260 tests; five pixel-creation tests failed because their setup lacked an allowlisted ad account. A further focused run passed 58 of 58 tests. Provider mutations were mocked, so no live ads were changed. These figures describe the review reproduction, not fixes for the six findings.

## Additional release checks

### Credential literals in tracked source

**Description:** The tracked PlatformConnectionSeeder still contains plaintext credential fallback literals. They remain because the temporary hardcoded fallback was explicitly requested, but encrypting values in the database does not protect literals in source code. Do not copy those values into this tracker.

**To do:** Review repository access and where this branch will be shared. Plan to remove the literals and rotate any real credentials before a wider release; keep the current behavior only within the agreed temporary scope.

### Resolve the five failing pixel tests

**Description:** The later 58-test focused pass does not turn the earlier 255/260 run into a clean run. The reported allowlist setup issue still needs to be fixed or reproduced with the intended fixture.

**To do:** Run the affected pixel tests with correct account setup, then rerun the relevant full suite after fixing the P1 issues. Record a clean result or a specific unresolved failure before release.

### Protect data before the connection migration

**Description:** The platform-connection migration decrypts existing platform_data with the current APP_KEY before separating public metadata from encrypted secrets. A key mismatch prevents this migration from completing.

**To do:** Take a restorable database backup and preserve the existing APP_KEY before deployment. Confirm that stored platform connections can still be read after migration; changing is_active alone does not repair decryption failures.

## Commands after release

### If deploy.sh completed successfully

**Description:** The script already runs migrations, refreshes Laravel caches, and restarts queue workers. Run these additional commands from the released project directory because the script does not seed countries or backfill targeting:

```bash
php8.5 artisan db:seed --class=CountryLocationSeeder --force --no-interaction
php8.5 artisan targeting:backfill --no-interaction
php8.5 artisan migrate:status --no-interaction
```

**Before running deploy.sh:** Resolve the open P1 chat and publishing findings above. The script invokes pf.sh; review and restrict its permission changes so deployment does not make .env or .env.bak readable by other users. Keep the existing APP_KEY; replacing it can make stored platform credentials undecryptable.

### If the release is deployed manually

**Description:** Run the commands in this order from the released project directory. Clearing cached configuration before seeding ensures the new country catalog is loaded. The seeder and backfill can be run again safely; do not run the full DatabaseSeeder or PlatformConnectionSeeder just for this location release.

```bash
php8.5 artisan migrate --force --no-interaction
php8.5 artisan optimize:clear --no-interaction
php8.5 artisan db:seed --class=CountryLocationSeeder --force --no-interaction
php8.5 artisan targeting:backfill --no-interaction
php8.5 artisan optimize --no-interaction
php8.5 artisan queue:restart --no-interaction
php8.5 artisan migrate:status --no-interaction
```

**Scope:** These commands prepare the local country and targeting tables. They do not create live campaigns, verify every provider location, or enable unfinished published-location edits.

## To do — finish Phase 2

### Validate live provider behavior

**Description:** Test Meta's city/region payload on a controlled paused ad set and Google's city/state import and criterion queries on an allowed account. Record non-secret API responses and resolve provider-version differences before broad rollout.

### Support approved edits to published locations

**Description:** Add Meta and Google in-place update paths with current provider-state comparison, account checks, targeting-revision snapshots, approval checks, and refreshed baselines. Preserve imported or unknown criteria and reject edits that silently expand an audience. Keep published-location edits blocked until this path and its regression tests pass.

### Determine LinkedIn city/state support

**Description:** Verify whether LinkedIn exposes reliable place type and country context for its mixed location typeahead results. If it does not, leave LinkedIn city/state selection unsupported rather than infer a broader location from a name or URN.

### Complete shared city intent

**Description:** Connect brief-level city intent to explicit platform selections without joining places by name alone. Test inherited, overridden, and cleared selections; imported campaigns; stale IDs; and unrelated edits. Compare new and legacy payloads before removing compatibility reads.

### Review and roll out the branch

**Description:** Review the full working tree, especially pf.sh, credential backups, the connection seeder, and new migrations. Run relevant tests after fixes. Migrate, seed countries, and run targeting:backfill independently in each target environment. This app has no frontend build step.

## To do — later phases

### Phase 3: markets, radius, and exclusions

**Description:** Verify platform DMA/market IDs, then add market selection, radius centers and units, exclusions, and presence/interest controls with platform capability checks. Do not bulk-seed unverified cities or DMAs.

### Phase 4: keywords

**Description:** Store selected terms, match types, negatives, and provenance at the correct delivery scope. Fix Google keyword research so geographic input reaches the provider.

### Phase 5: audience estimates

**Description:** Add account-scoped audience and forecast adapters. Cache by the complete targeting revision and request inputs, and show estimate type, age, and unavailable/error states.

### Connection management

**Description:** Add a UI for managing platform connections. Retire temporary hardcoded credential fallbacks only after stored connections and credential rotation are verified.
