# Facebook Engagements module feasibility study

**Assessment date:** 29 July 2026  
**Scope:** Repository-wide architecture and infrastructure review only. No Engagements feature code was implemented.  
**Proposed capability:** Manage personal Facebook browser sessions, inspect visible posts, generate account-specific comment suggestions, require explicit approval, and execute approved Likes or Comments through Playwright with manual takeover for all security challenges.

## Executive verdict

> **Final verdict: Technically possible but not recommended.**

The module is technically buildable, but it should **not** proceed to production in its proposed browser-automation form unless the business first obtains written confirmation that the intended use is permitted by Meta and completes a privacy/legal review. Meta's current Terms say users may not access or collect data from its products using automated means without prior permission, including while logged in, and its Automated Data Collection Terms require express written permission. See [Meta Terms of Service](https://www.facebook.com/terms/?locale=en_GB) and [Meta Automated Data Collection Terms](https://www.facebook.com/legal/automated_data_collection_terms). This is a launch-blocking policy risk, not a Playwright engineering problem.

If the policy gate is cleared, the technical infrastructure verdict is: **feasible only with a separate Playwright worker or VPS**. The MarkOps application can remain the control plane, but browser execution, a headed Linux desktop, and noVNC must run in an isolated service designed for long-lived browser processes. The repository has no Playwright/Chromium/noVNC dependencies (`package.json:5-24`, `whatsapp-server/package.json:1-12`), and its only production script configures PHP queue workers and cron on a root-managed Ubuntu/Debian server (`setup-production.sh:3-5`, `setup-production.sh:20-37`, `setup-production.sh:45-53`). There is no repository evidence that a shared cPanel host can install browser system packages, expose WSS/noVNC, or keep headed browser processes alive.

The existing Laravel authentication, tenancy, RBAC, database migrations, Inertia/Vue UI, AI service, and database queue are useful foundations. They are not sufficient as-is for account-isolated browser custody, durable manual intervention, exactly-once-like safeguards, or real-time browser streaming.

## 1. Evidence and confidence boundaries

This assessment reviewed all 290 tracked files, including 150 PHP files, 44 Vue files, all routes, configuration, migrations, services, tests, deployment scripts, and the Node WhatsApp sidecar. Important evidence boundaries are:

- The checked-in environment template is local-first: SQLite, database sessions/cache/queue, log broadcasting, and local storage (`.env.example:24-49`). The inspected active environment is also a local development configuration. It does not prove production topology.
- The only deployment artifact is `setup-production.sh`; there is no Dockerfile, Compose file, systemd unit, cPanel descriptor, CI deployment workflow, noVNC configuration, reverse-proxy configuration, or infrastructure-as-code file.
- Local Node is available, but Playwright is not installed in this project. Local PHP was not on the shell PATH during this assessment, so PHP tests could not be run. This does not establish what is installed on the production host.
- No server inventory is present. Production CPU, RAM, storage, operating system, Redis availability, process limits, and network/firewall capabilities remain **unverified**.
- Where the report says the database or framework “can support” a design, that means the repository has an appropriate extension point. It does not mean the proposed feature already exists.

## 2. Current technology stack

| Area | Current implementation | Repository evidence | Engagement implication |
|---|---|---|---|
| Backend | PHP 8.3+, Laravel 13 | `composer.json:9-12` | Suitable control-plane API, authorization, migrations, approvals, and audit records. |
| Server-rendered SPA bridge | Inertia Laravel 2 | `composer.json:10`; `resources/js/app.ts:4-17` | Suitable for adding an Engagements workspace without a separate frontend app. |
| Frontend | Vue 3, TypeScript, Vite 8, Tailwind | `package.json:11-24` | Suitable for campaigns, target matrix, approvals, job progress, and takeover UI. |
| Authentication | Laravel web session guard with Eloquent users | `config/auth.php:40-67`; `app/Http/Requests/Auth/LoginRequest.php:45`; `app/Http/Controllers/Auth/AuthenticatedSessionController.php:34-48` | Reusable for human users. Worker-to-app authentication must be separate. |
| API auth package | Sanctum installed | `composer.json:12` | Could support first-party API tokens, but no Engagement API or worker guard is currently configured. |
| RBAC | Spatie Laravel Permission | `composer.json:14`; `bootstrap/app.php:32-37`; `routes/web.php:38-54` | Reusable after adding granular Engagement permissions. |
| Tenancy | Shared database, `tenant_id`, Eloquent global scope | `app/Traits/BelongsToTenant.php:21-32`; `app/Models/Scopes/TenantScope.php:32-46` | Reusable, but unsafe to rely on alone in queue/CLI/worker contexts because the scope is disabled without an authenticated user. |
| Database | SQLite is the checked-in/local default; Laravel also configures conventional database drivers | `.env.example:24`; `composer.json:67-69`; `config/database.php` | Fine for a single-user POC; use PostgreSQL or MySQL for concurrent production jobs and leases. |
| Queue | Laravel database queue by default; jobs/batches/failed jobs tables exist | `.env.example:39`; `config/queue.php:37-44`; `database/migrations/0001_01_01_000002_create_jobs_table.php:14-44` | Good for short control-plane jobs, not for holding a live browser during indefinite manual takeover. |
| Queue worker management | Supervisor, two PHP workers, one-hour recycling | `setup-production.sh:25-37` | Shows server-managed workers are anticipated, but it does not manage Node/Chromium/noVNC. |
| Real-time | Broadcasting is configured to `log`; no Echo/Reverb/Pusher client packages | `.env.example:37`; `package.json:10-24` | No production real-time transport exists. Add SSE or Laravel Reverb/WebSockets for state events. |
| Storage | Private local disk rooted at `storage/app/private`; public disk exposed via `/storage` | `config/filesystems.php:33-46`; `.gitignore:19` | Private disk is the correct convention, but session profiles and private screenshots must never use the public disk. |
| AI | Custom service with DeepSeek, Groq, Gemini, and OpenAI fallbacks, tenant-specific settings and AI logs | `app/Services/AIService.php:32-89`, `app/Services/AIService.php:96-147`, `app/Services/AIService.php:301-312` | Reusable conceptually; add a typed Engagement prompt/output contract and stricter sensitive-data logging rules. |
| Existing Node service | Express/Baileys WhatsApp gateway with shared bearer secret and per-session directories | `whatsapp-server/server.js:49-76`, `whatsapp-server/server.js:122-151`; `app/Services/WhatsAppService.php:14-26` | Useful precedent for a sidecar, but it lacks the isolation and durable orchestration required for browser sessions. |

## 3. Current architecture

MarkOps is a Laravel monolith with an Inertia/Vue frontend. Web routes render Vue pages and accept form/AJAX commands (`routes/web.php:21-166`). Stateless webhook routes are mounted separately under `/webhooks` (`bootstrap/app.php:9-17`; `routes/webhooks.php:13-22`). The application uses Eloquent models with shared-schema tenancy and a database-backed queue. A separate local Node process provides WhatsApp connectivity over HTTP.

The existing sidecar pattern is relevant: Laravel calls a configured local service with a bearer secret (`.env.example:70-72`; `app/Services/WhatsAppService.php:19-26`), and the sidecar calls Laravel webhooks with the same secret (`whatsapp-server/server.js:100-118`). However, the sidecar keeps live sessions in process memory and stores authentication under a session-derived directory (`whatsapp-server/server.js:70-90`, `whatsapp-server/server.js:122-151`). That is not a production-grade template for Facebook because browser sessions are equivalent to account credentials, manual takeover must survive web requests, and path/session ownership must be more strongly constrained.

The tenant design is shared-schema. `BelongsToTenant` automatically stamps and filters `tenant_id` for authenticated web requests (`app/Traits/BelongsToTenant.php:48-70`), but `TenantScope` explicitly does nothing when no user is authenticated (`app/Models/Scopes/TenantScope.php:21-23`, `app/Models/Scopes/TenantScope.php:34-46`). A Playwright worker therefore must receive and verify explicit tenant/account identifiers and query every object with explicit ownership conditions. It must not assume the global scope protects worker execution.

## 4. Hosting and deployment findings

### What is proven

- The intended production script assumes root on Ubuntu/Debian, installs Supervisor with `apt-get`, runs two Laravel workers, and adds cron (`setup-production.sh:3-5`, `setup-production.sh:11-22`, `setup-production.sh:25-47`).
- The repository expects a continuously running local Node service for WhatsApp at `127.0.0.1:3000` (`.env.example:70-72`), but no Supervisor/systemd/PM2 definition starts it.
- There are no Playwright packages, browser binaries, Xvfb, VNC, noVNC, WebSocket server, Redis client dependency beyond Laravel's configuration, or container definition.

### What is not proven

The project does **not** establish that it is currently deployed on shared cPanel. It also does not prove that the production server allows continuous Node processes, root package installation, Chromium namespaces/sandboxing, Xvfb, noVNC ports, WebSockets, Redis, or adequate resources. Those facts require a host control-panel or SSH inventory. The generic Laravel README contains no deployment-specific evidence (`README.md`).

### Environment options

| Option | Technical result | Assessment |
|---|---|---|
| Current shared cPanel, if that is the host | Most shared plans do not grant root access for Chromium libraries/Xvfb/noVNC, may kill long-running processes, and may not proxy arbitrary WSS ports. None of these capabilities is proven here. | **Do not target it for Playwright.** MarkOps may remain there as the control plane if outbound HTTPS and callbacks are allowed. |
| Current server described by `setup-production.sh` | Root-managed Ubuntu/Supervisor could install the required packages, but co-locating untrusted browser sessions with the Laravel app increases blast radius and resource contention. | Technically possible after verification, but not recommended. |
| Separate VPS | Full control over containers, Chromium dependencies, Xvfb, noVNC, WSS, firewalling, encrypted volumes, and process supervision. | **Recommended POC and self-hosted production option.** |
| Separate Node Playwright worker server | Clean service boundary and independently scalable account concurrency. | **Recommended architecture.** Can be a VPS or container host. |
| Browser automation service | May supply managed browsers and live-view URLs. | Viable only if it supports persistent isolated profiles, secure interactive takeover, regional/privacy requirements, artifact deletion, and the intended Facebook use under its own and Meta's terms. |
| Hybrid | MarkOps owns users, approvals, records, AI, and audit; a separate worker owns browsers and streams. | **Best fit.** Avoids forcing browser dependencies onto the existing PHP host. |

## 5. Can Playwright, headed Chromium, and noVNC run here?

### Existing application host

Not verifiable and therefore not approved. There is no Playwright dependency in `package.json`, and `npm ls playwright playwright-core --depth=0` returned empty during this review. The deployment script installs only Supervisor and starts PHP queue workers (`setup-production.sh:20-37`). No X server, desktop/window manager, Chromium libraries, VNC server, noVNC proxy, TLS proxy, or browser process manager is configured.

### Separate worker/VPS

Yes. A Linux worker can run a pinned Playwright version and its matching Chromium build. Official Playwright documentation confirms that its container/runtime needs Node.js, browser binaries, and browser system dependencies, and headed Linux execution normally uses Xvfb; see [Playwright Docker](https://playwright.dev/docs/docker) and [Playwright CI](https://playwright.dev/docs/ci). For interactive takeover, run Chromium headed inside a private Xvfb display with a minimal window manager, x11vnc/TigerVNC, and noVNC/websockify behind an authenticated WSS gateway.

Do not expose VNC (5900) or raw noVNC directly to the internet. Only the gateway's HTTPS/WSS endpoint should be reachable, and only after MarkOps mints a one-time, session-bound ticket.

## 6. Authentication, authorization, and tenancy suitability

The existing human authentication is suitable. Laravel regenerates the session after login and invalidates it on logout (`app/Http/Controllers/Auth/AuthenticatedSessionController.php:34-48`), login attempts are rate limited (`app/Http/Requests/Auth/LoginRequest.php:45-82`), and routes already use role middleware (`routes/web.php:38-163`).

Required changes:

1. Add permissions such as `view engagements`, `manage facebook accounts`, `generate engagement suggestions`, `approve engagement actions`, `execute engagement actions`, `take over browser sessions`, and `view engagement audit` rather than granting the whole module to every authenticated user. The current permission seed has only broad marketing permissions (`database/seeders/RolesAndPermissionsSeeder.php:18-28`).
2. Add policies for every Engagement model. Route middleware alone is insufficient; every lookup must enforce `tenant_id` plus owner or authorized workspace membership.
3. Treat `user_id` as the personal-account owner and `tenant_id` as the workspace. A user should not automatically be allowed to operate another user's personal Facebook account merely because both are in the same tenant.
4. Require recent MarkOps re-authentication for viewing a live browser, deleting an account, rotating a session, or approving execution.
5. Use a dedicated worker identity. Do not give the worker a human session cookie or a broad Sanctum token. Prefer mutual TLS plus short-lived signed service JWTs, or HMAC requests with timestamp, nonce, body digest, narrow audience, and replay protection.
6. Worker callbacks must include immutable `tenant_id`, `facebook_account_id`, `action_id`, and `lease_id`; Laravel must re-validate all relationships before accepting an update.

## 7. Database suitability

Laravel migrations and Eloquent can represent the required data. Existing migrations demonstrate foreign keys, JSON, tenant indexes, and cascade deletion (`database/migrations/2026_05_22_000008_upgrade_social_accounts_for_oauth.php:26-65`; `database/migrations/2026_07_16_000000_create_whatsapp_accounts_table.php:14-23`). Existing social tokens are application-encrypted through Laravel Crypt (`app/Models/SocialAccount.php:71-111`). These are useful patterns.

SQLite is acceptable for the one-user POC but not recommended for production browser-job concurrency, lease contention, reporting, and many event writes. Production should use PostgreSQL or MySQL. Redis is recommended for queues/locks/events but is not proven available merely because Redis settings exist in `.env.example:46-49`.

### Improved logical schema

Use the project's `tenant_id` vocabulary rather than introducing `workspace_id` unless the whole product is renamed. Use string status columns plus application enums/check constraints rather than database enums, so the state machine can evolve.

#### `facebook_accounts`

- `id`, `tenant_id`, `owner_user_id`, `label`, `facebook_display_name`, `facebook_user_hint`
- `profile_mode` (`storage_state` or `persistent_profile`)
- `session_object_key` (opaque object-store key, never a client path), `session_key_version`, `session_checksum`
- `session_status`, `status`, `last_login_at`, `last_verified_at`, `last_used_at`, `session_invalidated_at`
- `profile_json` for language/dialect/tone/formality/emoji/about/allowed phrases/blocked phrases
- `lock_version`, timestamps, soft-delete timestamp
- Unique/indexes: `(tenant_id, id)`, `(tenant_id, owner_user_id, status)`. Do not assume Facebook display names are unique.

Do not store passwords, access tokens copied from page scripts, raw paths, or a boolean `manual_intervention_required`; intervention is a state/session record, not durable account identity.

#### `engagement_campaigns`

- `id`, `tenant_id`, `name`, `status`, `created_by_user_id`, timestamps, optional soft delete
- Index `(tenant_id, status, created_at)`.

#### `engagement_posts`

- `id`, `tenant_id`, `campaign_id`, `submitted_url`, `normalized_url`, `facebook_post_key`, `status`
- `extracted_content_json`, `extraction_version`, `content_hash`, `extracted_at`, `ai_analysis_json`
- `last_error_code`, timestamps
- Unique `(campaign_id, normalized_url)` and index `(tenant_id, status)`.

Store only content necessary for suggestion/review and delete it under a defined retention policy. Images/video should usually be metadata plus privacy-controlled artifacts, not permanent copies.

#### `engagement_targets`

This missing table should represent each account/post combination:

- `id`, `tenant_id`, `engagement_post_id`, `facebook_account_id`
- `requested_actions` JSON, `status`, `current_action_id`, timestamps
- Unique `(engagement_post_id, facebook_account_id)`.

This is the matrix row shown in the UI and avoids overloading posts or actions.

#### `engagement_suggestions`

- `id`, `tenant_id`, `engagement_target_id`, `generation_batch_id`, `version`
- `generated_comment`, `edited_comment`, `status`, `model`, `prompt_version`
- `approved_text_hash`, `approved_by_user_id`, `approved_at`, `rejected_by_user_id`, `rejected_at`, timestamps

Keep multiple rows/versions so regeneration does not overwrite audit history. Editing after approval must revoke approval and create a new version.

#### `engagement_actions`

Represent Like and Comment as separate rows because **each action requires separate explicit approval**:

- `id` (prefer UUID/ULID), `tenant_id`, `engagement_target_id`, `facebook_account_id`, optional `suggestion_id`
- `action_type`, `approved_payload_json`, `approved_payload_hash`, `status`
- `approved_by_user_id`, `approved_at`, `idempotency_key` (unique), `attempt_count`, `max_attempts`
- `worker_id`, `lease_token_hash`, `lease_expires_at`, `heartbeat_at`
- `error_code`, redacted `error_message`, `started_at`, `completed_at`, timestamps

An approval must freeze the exact payload. The worker executes only a payload whose hash matches the approval record.

#### `engagement_action_attempts`

Add a separate append-only attempt table with `action_id`, `attempt_no`, `worker_id`, `status`, navigation/result metadata, timestamps, and ambiguity flag. One action may have several attempts, while the user's intent and approval remain immutable.

#### `browser_sessions`

- `id` (ULID), `tenant_id`, `facebook_account_id`, `action_id`, `worker_id`
- `status`, `display_id`, `started_at`, `last_heartbeat_at`, `manual_requested_at`, `manual_expires_at`, `ended_at`
- `takeover_ticket_hash`, `takeover_ticket_used_at` (never store the plaintext ticket)
- `cancel_requested_at`, `failure_code`, timestamps

Only one active browser session per account should be enforced through an account lease plus a partial/functional uniqueness strategy supported by the chosen database.

#### `engagement_artifacts`

- `id`, `tenant_id`, `action_attempt_id`, `type`, opaque `object_key`, `sha256`, `size_bytes`, `mime_type`
- `classification`, `encryption_key_version`, `expires_at`, `created_at`, `deleted_at`

This is safer than storing screenshot paths on action rows and supports access control, lifecycle deletion, and quotas.

#### `engagement_audit_logs`

- `id`/ULID, `tenant_id`, `actor_type`, `actor_id`, optional account/action/session IDs
- `event`, `request_id`, `ip_hash`, redacted `metadata_json`, `created_at`
- Append-only permissions and an index `(tenant_id, created_at)`.

Record approvals, edits, takeover grants, continue/retry/cancel, worker claims, session save/delete, and artifact views. Do not log cookies, storage state, authorization headers, comment drafts beyond what is needed, or raw Facebook page HTML.

## 8. Browser-session design

### `storageState` versus persistent profile

Playwright's `storageState` covers cookies, local storage, IndexedDB, and WebAuthn/passkey state, but not session storage by default; see [Playwright authentication](https://playwright.dev/docs/auth). A persistent context stores broader browser-profile state in a dedicated `userDataDir`, and Playwright does not allow concurrent launches using the same directory; see [`launchPersistentContext`](https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context).

Recommendation:

- Start the POC by measuring both approaches with the test account. `storageState` is easier to encrypt, version, transport, and atomically replace. It may be sufficient if Facebook reliably restores the session across worker restarts.
- Plan production around a **dedicated persistent profile per Facebook account** if the POC shows Facebook depends on browser-profile state beyond storage state or frequently challenges fresh contexts. Persistent profiles are more likely to retain service-worker/cache/preferences and browser continuity, but are much larger and more sensitive.
- Never point Playwright at a user's normal Chrome profile. Use a worker-created directory dedicated to exactly one MarkOps account.
- Never launch two contexts against the same persistent directory. The per-account lease must cover the entire browser lifetime.

### Session lifecycle

1. **Create:** Laravel creates an account record and asks the worker to allocate a random internal profile ID. The worker resolves it under a fixed tenant/account root; it never accepts a filesystem path from Laravel or the browser.
2. **Login:** Worker launches headed Chromium in the isolated display. The user types credentials directly into Facebook through the takeover view. MarkOps and worker APIs never receive a password field.
3. **Verify:** After Continue, the worker checks multiple positive signals (expected Facebook origin, absence of known login/checkpoint UI, authenticated navigation, and account identity hint). Selector failure is “unknown/manual,” never automatic success.
4. **Save:** For storage state, write to a temporary file, encrypt with an account-scoped data-encryption key, checksum, upload to private storage, then atomically update the database pointer. For persistent profiles, close Chromium first and snapshot/archive the profile on encrypted block/object storage; never copy a live profile.
5. **Load:** Verify tenant/account ownership, key version, checksum, and active lease. Decrypt only into a worker-local directory with restrictive permissions and a randomized path.
6. **Refresh:** Save a new version after successful login/action. Keep at most one short-lived rollback version, then securely delete it under policy.
7. **Invalidate:** On logout/session-expired/checkpoint cancellation, mark the session unusable, revoke takeover tickets, and do not dispatch actions until revalidated.
8. **Delete:** Disconnect first prevents new leases, cancels queued work, closes active browsers, deletes encrypted objects/profile directories/artifacts, destroys the envelope key where supported, then records a non-sensitive deletion audit event.

### Encryption and isolation

- Use envelope encryption: a KMS/Vault-managed key-encryption key and a unique data key per Facebook account. Laravel's single `APP_KEY` pattern used by `SocialAccount` (`app/Models/SocialAccount.php:71-111`) is acceptable for a POC but creates too large a production blast radius.
- Encrypt object storage and worker disks. Store ciphertext/profile objects outside the web root; the existing private disk root is `storage/app/private` (`config/filesystems.php:33-37`). Never use the public disk (`config/filesystems.php:41-46`).
- Use OS/container isolation, a non-root browser user, per-session directories, strict filesystem permissions, CPU/memory/PID limits, and outbound firewall rules.
- Bind every object key to tenant/account IDs in server-generated metadata. Reject `..`, separators, symlinks, absolute paths, and unexpected resolved paths.

## 9. URL and navigation security

Accepting a Facebook URL is not sufficient protection because pages can redirect and user-visible links can navigate elsewhere.

- Parse using a real URL parser; require HTTPS, no username/password, no custom port, and a normalized hostname allowlist such as exact `facebook.com`, `www.facebook.com`, `m.facebook.com`, and explicitly reviewed subdomains. Reject lookalikes such as `facebook.com.example.org`.
- Normalize known post URL forms to a canonical key while retaining the submitted URL for audit.
- Before and after every navigation/redirect, enforce the allowlist. Block downloads, popups to non-allowed domains, `file:`, `data:`, `javascript:`, loopback/private/link-local IP destinations, and arbitrary CDP navigation.
- Use Playwright routing to block unnecessary third-party requests where doing so does not break Facebook, but do not alter behavior to evade detection.
- Disable arbitrary URL fields in worker APIs. A worker receives a validated post ID/action, then fetches the canonical URL from a signed command.
- Apply body limits, batch limits, per-user/account rate limits, and outbound bandwidth quotas.

## 10. Queue and job feasibility

The current queue has retries, failed-job persistence, batching tables, and Supervisor workers. `AttributionJob` demonstrates `$tries = 3`, backoff, failure logging, and dispatch to a named queue (`app/Jobs/AttributionJob.php:25-37`, `app/Jobs/AttributionJob.php:92-104`; `app/Observers/OrderObserver.php:124-128`). This is enough for ordinary asynchronous commands.

It is not enough for browser orchestration:

- The database queue's default `retry_after` is 90 seconds (`config/queue.php:37-44`), while a manual takeover can last many minutes. A long-running job could be redelivered while the original browser is still active.
- Supervisor recycles workers after 3,600 seconds (`setup-production.sh:28`) and manages only PHP.
- No per-account lock, idempotency contract, cancellation channel, progress event transport, worker heartbeat, lease recovery, or browser-process registry exists.
- A Laravel job must not block while waiting for a person. It should issue/claim a command and finish; the durable action/session state remains paused while the browser-session service stays alive.

### Required execution model

1. Laravel transaction creates the approved action plus an outbox/command row.
2. Worker claims it using an atomic account lease. One active lease per `facebook_account_id`.
3. Worker heartbeats both the action lease and browser session.
4. On security UI, worker persists `manual_intervention_required`, keeps the browser alive, emits an event, and stops automation input.
5. Continue/Retry/Cancel are new authenticated commands, not mutations of the original queued payload.
6. A reaper expires stale leases and manual sessions. It never retries an action whose result is ambiguous without reconciliation or new user approval.
7. On restart, the service reconciles database leases with surviving browser processes. If the browser is gone, mark the attempt interrupted and either restore a saved profile or require login.

For production, use Redis for command delivery, locks, and events if available, while keeping PostgreSQL/MySQL as the system of record. Laravel's database queue can support the POC if account leases are stored in SQL and browser waiting is outside the queue job.

### Idempotency and duplicate prevention

Browser automation cannot provide true exactly-once delivery. Use at-least-once commands plus reconciliation:

- Unique `idempotency_key` per approved Like or Comment; one database action row per intent.
- Like preflight reads the current state; if already liked by the expected account, treat it as reconciled rather than clicking again.
- Comment preflight searches the visible thread for the exact approved text authored by the expected account where feasible.
- After clicking, confirm UI state/comment presence and save evidence.
- If the browser crashes after submit but before confirmation, mark `outcome_unknown`. Do not automatically retry a Comment; ask the user to inspect/reconcile.
- Limit retries by error class. Navigation timeouts may be retryable; checkpoint, permission denial, deleted post, and ambiguous submit are not.

## 11. Manual takeover architecture

The browser must remain owned by the worker. Laravel only brokers authorization and state.

```text
MarkOps Vue UI
    | authenticated command/status API
Laravel Engagement control plane
    | signed short-lived worker commands + durable DB/queue state
Playwright session manager on isolated VPS
    | headed Chromium on private Xvfb display
VNC server -> noVNC/websockify -> HTTPS/WSS takeover gateway
    | one-time session/user/account-bound ticket
User browser
```

Safe frontend flow:

1. Vue receives only a browser-session ID and status, never a VNC password or reusable worker credential.
2. User clicks “Open secure browser.” Laravel verifies permission, tenant, account ownership, recent authentication, session status, and expiry.
3. Laravel returns a one-time ticket valid for roughly 30–60 seconds and bound to user, tenant, browser session, expected origin, and a single WebSocket upgrade. Store only its hash.
4. The UI opens a same-site reverse-proxied noVNC route in a sandboxed iframe or separate window. A separate window is safer if iframe clipboard/fullscreen permissions are unnecessary.
5. Gateway consumes the ticket, opens only the mapped private display, and immediately invalidates the ticket. It must not accept client-selected VNC hosts/displays.
6. On Continue, Laravel creates a signed command; the worker verifies login before saving the session and resuming. Retry re-runs only the safe current step. Cancel closes the context, revokes tickets, and marks the action cancelled.
7. A configurable timer closes Chromium after expiry. Warn before expiry and allow a policy-limited extension that requires authorization.

Mask clipboard/file transfer unless explicitly needed. Disable browser downloads and upload controls for the POC. Audit session open/close but do not record credentials or keystrokes.

## 12. API assessment

The proposed endpoint families are directionally correct, but execution commands should be explicit state transitions, not generic CRUD. Use the existing authenticated web/Inertia routes for UI commands or a new Sanctum-protected `/api/v1/engagements` group. Add policies, CSRF/session protection for same-origin requests, throttling, idempotency keys, and optimistic concurrency.

Recommended adjustments:

- `POST /facebook-accounts` creates metadata only; `POST /{account}/login-sessions` creates a browser session.
- Replace polling-only login endpoints with `GET /browser-sessions/{session}` plus status events.
- `POST /browser-sessions/{session}/takeover-ticket` mints the one-time viewer ticket.
- Keep Continue/Retry/Cancel under `browser-sessions` when they concern verification; keep action Retry/Cancel under actions.
- `POST /engagement-posts/{post}/analysis-runs` and `POST /engagement-targets/{target}/suggestion-batches` preserve version history.
- `POST /engagement-actions/{action}/approve` freezes the payload; `POST /execute` may be combined with approve-and-queue only if the UI clearly asks for both.
- Require a separate approval/action row for Like and Comment. “Like and Comment” creates two approvals and two commands.
- Worker endpoints should be separate, e.g. `/internal/v1/worker/commands/claim`, `/heartbeat`, `/events`, and `/artifacts`, inaccessible to browser users.
- Every state-changing endpoint accepts an `Idempotency-Key`. Return `409` for invalid transitions and `202` for accepted asynchronous commands.

The existing `routes/api.php` contains a public signed-link action rather than a general authenticated API (`routes/api.php:8-42`), so this module needs deliberate API middleware rather than copying that pattern.

## 13. Playwright worker behavior

The worker should implement a versioned state machine and structured result contract, not a collection of brittle selector scripts.

For each action:

1. Verify signed command, approval hash, tenant/account relationship, idempotency key, and active account lease.
2. Materialize the encrypted session into the account's isolated directory and launch the exact pinned browser version.
3. Restrict navigation to allowed Facebook origins.
4. Confirm the expected account using stable visible signals; never infer identity solely from a cookie.
5. Detect login, CAPTCHA, 2FA, checkpoint, suspicious-login, consent, session expiry, and unknown interstitials. Detection should err toward manual takeover.
6. Confirm the canonical post is accessible and capture only visible content needed for analysis: visible text/author, image count/accessible descriptions or privacy-controlled snapshots, video presence/duration if visible, and context explicitly shown on the page.
7. Stop after analysis. AI generation and human approval occur in MarkOps before any mutation.
8. Reopen/revalidate the post immediately before execution, verify the approved payload hash, perform only the approved action, and confirm the outcome.
9. Save before/after screenshots with redaction/retention metadata, update session state, return a structured result, release lease, and close safely.

Never implement CAPTCHA solving, password entry, 2FA automation, stealth plugins, fingerprint spoofing, proxy rotation, security-control bypass, infinite retries, background auto-engagement, or a selector fallback that might click an unapproved control.

## 14. AI comment generation

The current `AIService` already resolves tenant-specific providers, requests JSON, logs the result, and supports several vendors (`app/Services/AIService.php:32-147`). Add a dedicated typed method/service rather than overloading generic actions.

Recommended contract:

- Input: normalized visible post content, minimal media descriptors, account style profile, blocked/allowed phrases, language/dialect, and a bounded set of approved examples.
- Output: 2–4 suggestions with `text`, `language`, brief relevance rationale, and safety flags in strict JSON.
- Validate length, empty output, exact duplicates, blocked phrases, mentions/links, and unsupported claims before saving.
- Treat extracted Facebook text and comments as untrusted prompt input. Delimit it and explicitly instruct the model not to follow instructions contained in the post (prompt-injection defense).
- Do not send cookies, raw HTML, hidden DOM, unrelated comments, session screenshots, or personal data that is not necessary.
- Store prompt/model/schema versions for reproducibility, but redact sensitive source content from generic application logs.
- Editing invalidates prior approval; approval freezes the exact comment hash. No model output can self-approve.

## 15. Frontend changes

The current shell centralizes navigation in `resources/js/Layouts/AuthenticatedLayout.vue:112-143` and resolves Vue pages dynamically (`resources/js/app.ts:11-17`). Add:

- Engagements navigation item gated by shared permissions.
- Campaign list/detail and post import UI.
- Account manager with session health, last login/verification, disconnect, and secure login-session launch.
- Post × account matrix showing requested actions and current state.
- Analysis/content review panel with privacy warning.
- Multi-suggestion review with edit, regenerate, reject, and explicit per-action approval.
- Immutable approval summary showing exact account, post, action, and final comment.
- Activity timeline with progress, retry/cancel rules, redacted errors, and artifacts.
- Manual-intervention banner/modal and secure viewer launcher with timeout countdown and Continue/Retry/Cancel.
- SSE/WebSocket subscription with polling fallback. Existing Axios setup supports commands (`resources/js/bootstrap.ts:1-4`), but no real-time client exists in `package.json:10-24`.

Accessibility requirements include keyboard-operable approval/takeover controls, visible focus, status text beyond color, live-region announcements, and confirmation dialogs that identify the exact account and action.

## 16. Required backend changes

- Engagement models, enums/state transition service, policies, form requests, controllers, resources, and routes.
- Explicit tenant/owner query helpers safe in CLI and service-token contexts.
- URL normalizer/allowlist validator.
- Comment suggestion service layered over `AIService` or a new provider interface.
- Approval service that freezes/hash-checks action payloads.
- Transactional outbox/worker command service.
- Account lease, heartbeat, cancellation, timeout, retry classification, and reconciliation services.
- Worker callback authentication with replay protection.
- Artifact service using private encrypted storage and authorization-controlled streaming responses.
- Audit logger with redaction and append-only access policy.
- Notifications/status events through SSE or broadcasting.
- Scheduled reapers for expired manual sessions, stale leases, artifacts, and disconnected-account data.

Do not reuse `social_accounts` for personal Facebook browser identities. That model represents OAuth/API platform accounts and encrypted tokens (`app/Models/SocialAccount.php:13-25`, `database/migrations/2026_05_22_000008_upgrade_social_accounts_for_oauth.php:8-20`). Browser session custody has different lifecycle, risk, and ownership rules.

## 17. Worker service and communication recommendation

Use this split:

```text
Current MarkOps Application
        -> Engagement control-plane API and SQL system of record
        -> transactional outbox / command queue
        -> separate Node.js Playwright session manager
        -> headed Chromium + private Xvfb/VNC/noVNC gateway
        -> Facebook
```

### POC communication

Use worker-pull HTTPS REST:

- Laravel stores durable commands in SQL.
- Worker polls/long-polls a private claim endpoint, atomically obtains a lease, and posts heartbeats/events.
- Requests use mTLS plus short-lived signed service tokens (HMAC with replay prevention is an acceptable POC fallback).
- Laravel pushes UI state through SSE or falls back to 2–5 second polling.

This works even if MarkOps remains on hosting where Redis is unavailable and avoids exposing the worker's control port publicly.

### Production communication

Use PostgreSQL/MySQL as system of record, Redis queues/locks/pub-sub for low-latency commands, and Laravel Reverb or another WebSocket/SSE service for frontend events. Keep signed internal requests even when Redis is private. noVNC pixel/input traffic should go directly through the dedicated authenticated WSS gateway; do not relay it through PHP.

Queue messages are preferred over synchronous “execute now” REST because execution is long-running and recoverable. REST remains appropriate for health, signed command claim/ack, and artifact upload. WebSockets are for UI/noVNC transport, not the source of truth.

## 18. Security and privacy risk register

| Risk | Severity | Required mitigation |
|---|---:|---|
| Stolen browser session gives Facebook account access | Critical | Per-account envelope encryption, encrypted disks, least-privilege worker, short materialization lifetime, no public storage, key rotation, access audit. |
| Cross-tenant/account browser viewing | Critical | Explicit tenant + owner policies, session-bound one-time viewer tickets, gateway-side mapping, recent re-authentication, no client-selected display. |
| Worker compromise exposes many accounts | Critical | Separate worker pool, container/OS isolation, shard keys, outbound allowlist, minimal secrets, patching, EDR/monitoring, limited concurrent materialized profiles. |
| Unauthorized or modified action after approval | Critical | Immutable approval record and payload hash, separate approval per Like/Comment, worker hash verification, transition policy, idempotency key. |
| Meta terms/enforcement/account suspension | Critical | Written Meta authorization/legal approval before production; test account only in POC; kill switch and user disclosures. |
| Path traversal/symlink attacks | High | Server-generated opaque IDs, canonical resolved-root checks, no paths in APIs, reject separators/symlinks, restrictive permissions. |
| Arbitrary navigation/SSRF-like browser abuse | High | HTTPS Facebook hostname allowlist before/after redirects, block private IP schemes/downloads/popups, request quotas. |
| Screenshots expose messages, names, notifications, or credentials | High | Capture only scoped moments, redact/crop where possible, encrypted private artifacts, short retention, authorized streaming, no login-page screenshots by default. |
| Logs expose cookies/content/tokens | High | Structured allowlisted logs, header/cookie/body redaction, avoid DOM/HTML dumps, separate protected audit log. Current services log session identifiers (`app/Services/WhatsAppService.php:60-73`), so the browser service needs stricter rules. |
| Duplicate Comment after ambiguous failure | High | Reconciliation, exact-text/author check, `outcome_unknown`, no automatic comment retry, user decision. |
| AI prompt injection or privacy leakage | High | Treat post text as data, minimize inputs, vendor agreements/retention controls, output validation, no session artifacts to AI. |
| Orphan browsers/resource exhaustion | High | Heartbeats, TTL/reaper, per-user/account quotas, CPU/RAM/PID limits, safe shutdown, worker admission control. |
| Takeover token replay | High | Store hash only, 30–60 second TTL, single use, bind user/session/origin, revoke on close, WSS/TLS. |
| Account removal leaves session copies | High | Two-phase disconnect/delete, cancel and close, delete all versions/artifacts/temp files, cryptographic erasure, deletion audit. |

Privacy policy must define purpose, legal basis, user notice/consent, data-subject handling, vendor/subprocessor list, retention, incident response, and deletion. Extract only content visible to the connected account and necessary for the requested post; visibility to an account does not automatically authorize copying or AI processing.

## 19. Reliability and maintenance risks

- Facebook DOM, accessibility labels, dialogs, localized text, and experiments change frequently. Selectors require continuous maintenance and multi-locale fixtures.
- Security challenges are non-deterministic. A saved session can expire at any time, and automation itself may cause checkpoints or account restrictions.
- Like/comment confirmation is UI-based and can be ambiguous under latency, moderation, deleted posts, restricted comments, or duplicate text.
- Different post surfaces (permalink, group, reel, photo, share, mobile URL) need separate extraction/action adapters.
- Headed Chromium consumes significant RAM/CPU; video and many images increase bandwidth/storage.
- Browser/Playwright versions must be pinned and upgraded together. Official Playwright guidance notes version matching for remote/container browsers.
- noVNC, Xvfb, Chromium, OS libraries, certificates, and reverse proxy become an additional patching surface.
- Facebook account status and product terms can change independently of application releases.

Use feature flags, selector/version telemetry, canary test accounts, a global execution kill switch, per-account rate limits, and an operational runbook. Never respond to breakage by adding stealth or bypass techniques.

## 20. Infrastructure requirements

### POC VPS baseline

- Ubuntu 24.04 LTS or another Playwright-supported glibc Linux distribution.
- 2–4 vCPU, 8 GB RAM, 40–80 GB encrypted SSD, and swap as a safety net for one headed browser. Measure actual peak usage before increasing concurrency.
- Docker with a pinned Playwright-compatible image, or Node.js LTS plus pinned Playwright and `playwright install --with-deps`.
- Xvfb, minimal window manager, x11vnc/TigerVNC, noVNC/websockify, and Nginx/Caddy for TLS/WSS.
- Supervisor/systemd/Docker restart policies for worker, session manager, gateway, and reaper.
- Firewall: inbound HTTPS/WSS only through the reverse proxy; VNC and worker control ports private; outbound restricted to required Facebook, MarkOps, artifact/KMS, and monitoring endpoints.
- Encrypted persistent volume or private encrypted object storage, KMS/Vault or a POC master key, centralized redacted logs, metrics, and alerting.

Eight GB is a conservative POC starting point, not a verified production sizing result. Benchmark login, post/video rendering, screenshots, and noVNC before choosing accounts-per-worker. Production should admit work based on measured memory and CPU, commonly starting with one active headed browser per 1–2 GB of available RAM plus OS/service headroom.

### Required server packages/services

- Node.js LTS and npm/pnpm; pinned `playwright` package and bundled Chromium.
- Playwright Linux dependencies, font packages for Arabic/emoji, CA certificates.
- Xvfb, minimal window manager, VNC server, noVNC/websockify.
- TLS reverse proxy, process manager/container runtime, log rotation, time synchronization.
- Redis for production queues/locks/events (recommended, not required for POC).
- PostgreSQL or MySQL production database.
- KMS/Vault or equivalent secret/key service; encrypted object/block storage.

## 21. Required environment variables

Names should be finalized in configuration and secret management; do not put real values in source control.

```dotenv
ENGAGEMENTS_ENABLED=false
ENGAGEMENTS_EXECUTION_ENABLED=false
ENGAGEMENT_WORKER_BASE_URL=
ENGAGEMENT_WORKER_AUDIENCE=
ENGAGEMENT_WORKER_SIGNING_KEY=
ENGAGEMENT_WORKER_MTLS_CERT=
ENGAGEMENT_WORKER_MTLS_KEY=
ENGAGEMENT_COMMAND_QUEUE=engagements
ENGAGEMENT_ACCOUNT_LOCK_TTL_SECONDS=120
ENGAGEMENT_HEARTBEAT_SECONDS=20
ENGAGEMENT_MANUAL_TIMEOUT_SECONDS=900
ENGAGEMENT_MAX_RETRIES=2
ENGAGEMENT_MAX_POSTS_PER_CAMPAIGN=50
ENGAGEMENT_ALLOWED_FACEBOOK_HOSTS=facebook.com,www.facebook.com,m.facebook.com
ENGAGEMENT_SESSION_STORAGE_DISK=engagement_sessions
ENGAGEMENT_ARTIFACT_STORAGE_DISK=engagement_artifacts
ENGAGEMENT_SESSION_KMS_KEY_ID=
ENGAGEMENT_ARTIFACT_RETENTION_DAYS=7
ENGAGEMENT_AUDIT_RETENTION_DAYS=365
ENGAGEMENT_TAKEOVER_PUBLIC_URL=
ENGAGEMENT_TAKEOVER_TICKET_TTL_SECONDS=60
ENGAGEMENT_SCREENSHOT_ENABLED=true
ENGAGEMENT_SCREENSHOT_LOGIN_PAGES=false
REDIS_HOST=
REDIS_PASSWORD=
REDIS_PORT=6379
BROADCAST_CONNECTION=reverb
```

Worker-specific configuration should include MarkOps internal URL, service identity, browser/profile roots, Chromium version/path, display range, allowed hosts, concurrency, storage/KMS, artifact limits, proxy trust, and redaction rules. Facebook usernames/passwords must never be environment variables.

## 22. Minimal proof of concept

### Preconditions and stop/go gates

1. Written internal authorization for a single test account owned by the tester and a policy/legal decision on Meta automation. Production remains disabled.
2. Separate non-production VPS and isolated MarkOps environment/data.
3. One test account, one known test post, one user, one worker, concurrency one.
4. Feature flags default off; global kill switch tested.

### POC sequence

1. Add account metadata in MarkOps; no password field exists.
2. Launch headed Chromium in a new isolated profile and show it using the one-time noVNC flow.
3. User manually logs in and completes all challenges.
4. Continue verifies expected account and saves encrypted state/profile.
5. Close and restart the browser to prove session reuse.
6. Submit and normalize one Facebook post URL; worker validates every navigation and extracts only visible content.
7. AI generates multiple suggestions for the one account; user edits/selects and approves the exact Comment.
8. Create separate explicit approvals for Like and Comment.
9. Execute Like, confirm UI state, save before/after artifacts.
10. Execute the approved Comment, confirm it appears, save artifacts.
11. Force or simulate `manual_intervention_required`, verify that automation stops, viewer authorization is isolated, Continue revalidates, and the original action resumes.
12. Test Cancel, timeout, worker crash, expired session, deleted/restricted post, ambiguous post-submit result, duplicate command, and account disconnect cleanup.

### POC acceptance criteria

- No password, cookie, storage state, viewer ticket, or authorization header appears in Laravel/worker logs or database plaintext.
- Reusing the saved session works after a worker restart.
- Two simultaneous commands for the same account cannot run.
- A modified/unapproved comment is rejected by the worker.
- Replaying command/takeover tokens fails.
- Every transition and approval is auditable; artifacts are tenant/owner protected and expire.
- Cancel/timeout closes the browser, and disconnect removes all session material.
- No automatic action occurs during any login/verification/unknown screen.

## 23. Implementation phases and estimates

These are engineering ranges for one experienced full-stack engineer with DevOps support, after the policy gate. They are not fixed-price commitments.

| Phase | Scope | Estimate |
|---|---|---:|
| 0. Policy, threat model, host inventory | Meta authorization decision, privacy review, data map, abuse cases, VPS verification | 3–7 working days plus external review time |
| 1. Control-plane foundation | Migrations/models/policies/permissions, state machines, URL validation, account/campaign UI, feature flags | 1.5–2.5 weeks |
| 2. Worker and manual login POC | Playwright service, persistent session, Xvfb/noVNC gateway, one-time tickets, login/continue/cancel | 2–3 weeks |
| 3. Extraction, AI, approvals | Post adapters, structured extraction, account profiles, suggestion versions, exact approval hashing | 1.5–2.5 weeks |
| 4. Execution and durability | Like/comment adapters, leases, idempotency/reconciliation, screenshots, retries, restart recovery | 2–3 weeks |
| 5. Hardening and operations | Security tests, quotas/retention, metrics/alerts, failure drills, cross-tenant tests, runbooks | 2–4 weeks |

Expected POC: roughly **4–7 weeks** after infrastructure/policy prerequisites. Production hardening: roughly **9–15 total engineering weeks**, with ongoing selector/operations maintenance. A managed browser provider may reduce desktop infrastructure work but increases vendor/privacy assessment work.

## 24. Exact files to create or modify

This is the expected file-level plan; final names may be refined during implementation.

### Modify existing MarkOps files

- `composer.json` — add real-time/SDK dependencies only if selected.
- `package.json` and lock file — add frontend real-time client if selected; do **not** add Playwright to the MarkOps frontend package when using a separate worker.
- `.env.example` — add the non-secret Engagement configuration names.
- `config/queue.php` — add the Engagement queue/connection and appropriate retry settings for short control jobs.
- `config/filesystems.php` — add private session/artifact disks with visibility and lifecycle expectations.
- `config/services.php` — add worker internal endpoint/auth configuration.
- `config/logging.php` — dedicated redacted Engagement/audit channels.
- `bootstrap/app.php` — internal worker middleware aliases/rate limits if required.
- `routes/web.php` — authenticated Engagement page and human command routes.
- `routes/api.php` — versioned worker/control API, or require new route files from `bootstrap/app.php`.
- `routes/console.php` — schedule lease/session/artifact cleanup.
- `database/seeders/RolesAndPermissionsSeeder.php` — granular Engagement permissions.
- `app/Http/Middleware/HandleInertiaRequests.php` — share Engagement permissions/feature flags, not secrets.
- `resources/js/Layouts/AuthenticatedLayout.vue` — permission-gated Engagements navigation/status indicator.
- `resources/js/types/index.d.ts` — shared account/campaign/action/session types.
- `app/Services/AIService.php` — only if extending its public typed contract; provider calls should otherwise remain unchanged.
- `setup-production.sh` — keep PHP control-plane workers; document that browser services are separately deployed. Do not bolt noVNC onto this script without an explicit host design.

### Create MarkOps backend files

- `config/engagements.php`
- `routes/engagements.php`
- `routes/internal.php`
- Models: `app/Models/FacebookAccount.php`, `EngagementCampaign.php`, `EngagementPost.php`, `EngagementTarget.php`, `EngagementSuggestion.php`, `EngagementAction.php`, `EngagementActionAttempt.php`, `BrowserSession.php`, `EngagementArtifact.php`, `EngagementAuditLog.php`, `EngagementOutbox.php`, `FacebookAccountLease.php`
- Enums/state: `app/Enums/FacebookSessionStatus.php`, `EngagementStatus.php`, `EngagementActionStatus.php`, `BrowserSessionStatus.php`, `EngagementActionType.php`
- Policies for each externally addressable Engagement model under `app/Policies/`
- Requests under `app/Http/Requests/Engagements/` for URL import, account profile, suggestion edit, approval, execute, Continue/Retry/Cancel, and takeover ticket
- Controllers under `app/Http/Controllers/Engagements/` and worker callbacks under `app/Http/Controllers/Internal/`
- API resources under `app/Http/Resources/Engagements/`
- Services under `app/Services/Engagements/`: `FacebookUrlNormalizer`, `EngagementAuthorizationService`, `SuggestionService`, `ApprovalService`, `ActionStateMachine`, `AccountLeaseService`, `WorkerCommandService`, `ArtifactService`, `AuditService`, `SessionCleanupService`
- Middleware under `app/Http/Middleware/`: `AuthenticateEngagementWorker`, `RequireRecentAuthentication`, `ValidateIdempotencyKey`
- Events/notifications for state changes and manual takeover under `app/Events/Engagements/` and `app/Notifications/`
- Short queue jobs under `app/Jobs/Engagements/` for command publication, AI generation, notifications, and cleanup; no job should sleep waiting for manual input
- Console commands under `app/Console/Commands/` for reconciliation and cleanup

### Create migrations

- `create_facebook_accounts_table`
- `create_engagement_campaigns_table`
- `create_engagement_posts_table`
- `create_engagement_targets_table`
- `create_engagement_suggestions_table`
- `create_engagement_actions_table`
- `create_engagement_action_attempts_table`
- `create_browser_sessions_table`
- `create_engagement_artifacts_table`
- `create_engagement_audit_logs_table`
- `create_engagement_outbox_table`
- `create_facebook_account_leases_table`

Use the next timestamp prefix at implementation time rather than preselecting dates in this report.

### Create frontend files

- `resources/js/Pages/Engagements/Index.vue`
- `resources/js/Pages/Engagements/Campaign.vue`
- `resources/js/Pages/Engagements/Accounts.vue`
- `resources/js/Pages/Engagements/ActionReview.vue`
- Components under `resources/js/Components/Engagements/`: `AccountCard.vue`, `PostImporter.vue`, `TargetMatrix.vue`, `SuggestionReview.vue`, `ApprovalDialog.vue`, `ActionTimeline.vue`, `ManualTakeoverDialog.vue`, `BrowserViewer.vue`, `StatusBadge.vue`
- Composables under `resources/js/composables/`: `useEngagementEvents.ts`, `useBrowserTakeover.ts`
- Types under `resources/js/types/engagements.ts`

### Create tests

- Feature tests for every policy/ownership boundary, state transition, approval hash, idempotency key, worker signature/replay, URL allowlist, artifact authorization, cleanup, and API command.
- Unit tests for URL normalization, state machine, leases, prompt/output validation, redaction, and signature verification.
- Browser worker tests with local fixture pages for logged-in, login, 2FA, CAPTCHA, checkpoint, consent, inaccessible post, Like, Comment, and ambiguous result states. Do not exercise real Facebook in normal CI.
- End-to-end POC test checklist executed manually against the owned test account.

### Create a separate worker service (prefer a sibling repository/deployment)

- `engagement-worker/package.json` and lock file with pinned Playwright.
- `src/server.ts`, `src/config.ts`, `src/commands/`, `src/sessions/SessionManager.ts`, `src/browser/FacebookAdapter.ts`, `src/browser/SecurityPageDetector.ts`, `src/browser/PostExtractor.ts`, `src/browser/ActionExecutor.ts`, `src/storage/EncryptedProfileStore.ts`, `src/security/CommandVerifier.ts`, `src/security/UrlPolicy.ts`, `src/artifacts/ArtifactUploader.ts`, `src/telemetry/`
- `Dockerfile`, health check, non-root user, pinned browser image/version.
- noVNC/Xvfb/window-manager startup configuration and WSS gateway configuration.
- Supervisor/systemd/Compose/Kubernetes configuration appropriate to the chosen VPS/platform.
- Unit/integration fixtures and operational runbooks for upgrades, stuck sessions, account deletion, incidents, and emergency shutdown.

## 25. Decision and next step

Do not implement the complete module now. The next authorized step should be a **policy-gated, isolated POC design review**:

1. Obtain a written Meta/legal decision for the intended automated access and engagement behavior.
2. Collect the real hosting inventory: OS/control panel, root access, process limits, Node support, outbound networking, reverse-proxy/WSS support, Redis/database, CPU/RAM/storage, backup and encryption capabilities.
3. Provision a separate non-production VPS and produce a threat model plus data-retention policy.
4. Implement only manual login, encrypted session persistence/reuse, secure viewer isolation, and one read-only post extraction first.
5. Add approved Like/Comment only after the session/security foundation passes the acceptance criteria and the policy gate remains cleared.

Without those gates, MarkOps should prefer official Meta APIs and supported product workflows. The existing application already uses official Meta OAuth/API patterns for Pages/ad accounts (`routes/web.php:148-163`; `app/Http/Controllers/SocialConnectController.php:30`; `app/Models/SocialAccount.php:13-25`), which are operationally and contractually safer than personal-account browser automation.
