# Filament Peek — Improvement & Growth Plan

> Package: capell-app/filament-peek · Kind: package · Tier: free · Product group: Capell Foundation · Bundle: foundation · Status: Complete

## 1. Snapshot

`capell-app/filament-peek` is a thin first-party integration that wires the upstream `pboivin/filament-peek` Filament plugin (`composer.json` `require`) into Capell Admin, then layers Capell-specific value on top: it adds a `Changes` preview action to the Page edit header (`src/Filament/Actions/PeekPagePreviewAction.php`), captures the editor's **unsaved** Page form state plus live Layout Builder state into a short-lived cache snapshot (`src/Actions/CreatePagePreviewSnapshotAction.php`, `StoreLayoutBuilderPreviewStateAction.php`), and renders that snapshot through the real Capell frontend pipeline behind a temporary signed route (`src/Actions/RenderPagePreviewSnapshotAction.php`, `src/Http/Controllers/PagePreviewController.php`). The upstream plugin only supplies the iframe modal shell and `open-preview-modal` Livewire event; **all of the snapshot, render, auth, and Layout-Builder-overlay logic is genuinely first-party** and is the bulk of the 1,307 LoC under `src/`. It owns no DB tables/settings (`capell.json` `database.migrations: false`), surfaces on `admin` + `frontend`, and declares one capability (`page-preview`). Marketplace summary now leads with unsaved Page and Layout Builder previews through private, expiring, signed live-theme links. Marketplace screenshots in `capell.json` now include the extension card and real Capell runner captures for the Page edit preview action group; the older static desktop/mobile hero artwork is no longer promoted as product media. The signed modal and expired-preview product states remain optional runner follow-ups because the current runner cannot perform sequential grouped-action clicks or mint temporary signed route URLs during capture.

## 2. Improvements (existing functionality)

- **Done / Shipped: remove the dead `path` field from the snapshot.** — Evidence: `PagePreviewSnapshotData` now contains only token/user/page/form/workspace/Layout Builder state, `CreatePagePreviewSnapshotAction` no longer writes `path`, and `rg "path:" packages/filament-peek/src packages/filament-peek/tests` finds no snapshot-path assignment. — `src/Data/PagePreviewSnapshotData.php`, `src/Actions/CreatePagePreviewSnapshotAction.php` — **S**
- **Done / Shipped: move snapshot read into a dedicated action.** — Evidence: `FindPagePreviewSnapshotAction` owns cache-key lookup and DTO hydration, while `PagePreviewController`, `PeekPagePreviewActionTest`, and `SnapshotActionTest` call `FindPagePreviewSnapshotAction::run($token)` instead of reading through the create action. — `src/Actions/FindPagePreviewSnapshotAction.php`, `src/Http/Controllers/PagePreviewController.php`, `tests/Unit/SnapshotActionTest.php` — **M**
- **Done / Shipped: de-duplicate preview context helpers.** — Evidence: `CreatePagePreviewSnapshotAction`, `FindPagePreviewSnapshotAction`, and `StoreLayoutBuilderPreviewStateAction` all use `Concerns\ResolvesPreviewContext` for Filament/Auth user fallback, preview cache store resolution, TTL clamping, snapshot cache keys, and Layout Builder preview cache keys. — `src/Concerns/ResolvesPreviewContext.php`, `src/Actions/CreatePagePreviewSnapshotAction.php`, `src/Actions/StoreLayoutBuilderPreviewStateAction.php` — **S**
- **Done / Shipped: device/responsive preview controls.** — Capell now bridges `capell-filament-peek.preview.device_presets` into the upstream `filament-peek.devicePresets` modal config after package config is merged, with fullscreen/tablet/mobile defaults and provider/action coverage. — `config/capell-filament-peek.php`, `src/Providers/FilamentPeekServiceProvider.php`, `tests/Unit/FilamentPeekProviderTest.php`, `tests/Feature/PeekPagePreviewActionTest.php` — **M**
- **Done / Shipped: visible admin-only preview ribbon.** — Preview responses now inject a private "Unsaved preview - not published" ribbon into rendered HTML while the route keeps `private, no-store` and `X-Robots-Tag` headers. The ribbon is rendered only by the signed preview action path and is covered by route tests. — `src/Actions/RenderPagePreviewSnapshotAction.php`, `resources/views/preview-ribbon.blade.php`, `resources/lang/en/actions.php`, `tests/Feature/PagePreviewRouteTest.php` — **M**
- **Cap snapshot payload size.** — `formState` and `layoutBuilderState.containers` are serialized verbatim into the cache with no size guard; a large Layout Builder page can push a multi-MB blob per click per user. Add a soft size check / log-and-truncate before `Cache::put`. — `src/Actions/CreatePagePreviewSnapshotAction.php`, `src/Actions/StoreLayoutBuilderPreviewStateAction.php` — **S**
- **Reconcile `capell.json` `adminQueryBudget: 40` with the real render cost.** — `RenderPagePreviewSnapshotAction::renderSnapshot()` eager-loads ~12 relations and may issue additional `find()` queries in `resolveSite`/`resolveLayout`/`previewTranslations` (per-language `Language::find`) and `RegisterLayoutBuilderPreviewWidgetsAction` (widgets + assets + target models). The 40-query admin budget likely doesn't reflect the frontend render path (whose `frontendRenderBudgetMs` is `0`/unset). Add a budget that covers the preview render and a regression test asserting query count. — `capell.json`, `src/Actions/RenderPagePreviewSnapshotAction.php` — **M**

## 3. Missing Features (gaps)

Capability declared: `capabilities: ["page-preview"]`. Against common CMS preview norms:

- **Done / Shipped: responsive / device-frame preview** — Capell exposes upstream Peek device presets through package config with fullscreen, tablet, and mobile defaults. Remaining depth is product screenshot recapture of the modal states.
- **Share-a-preview link for non-editors (reviewer/stakeholder)** — currently a preview is locked to the creating admin (`userOwnsSnapshot`, `PagePreviewController.php:74-87`). There is no way to send a client/editor-without-edit-rights a read-only preview. This is the single biggest _differentiator_ gap, and it **overlaps directly with publishing-studio's `WorkspacePeekPreviewAction` + `GenerateWorkspacePreviewUrlAction`** (`packages/publishing-studio/.../Actions/WorkspacePeekPreviewAction.php`), which already mints workspace preview URLs via the same upstream `Peek` modal and `open-preview-modal` event. Decide ownership: either filament-peek owns a generic "shareable preview token" primitive that publishing-studio consumes, or this stays workspace-only. Today they are two parallel, independently-built preview actions sharing only the upstream facade.
- **Preview of draft / unpublished pages as such** — the render path overlays unsaved form state but always authorizes via `Gate::authorize('update', $page)` and renders through the live pipeline; there's no explicit "preview this scheduled/`visible_from`-future page" mode. `visible_from`/`visible_until` are copied into `pageAttributes()` but their effect on the rendered output is untested. Gap (partial).
- **Live/auto-refresh preview** — snapshot is a point-in-time blob; editing in the form does not live-update the open preview iframe. Upstream has a builder-editor live-preview mode that isn't wired. _Differentiator_ gap.
- **Builder-editor (inline) preview** — `pboivin/filament-peek` ships a "Builder Editor" side-by-side preview; Capell only uses the modal iframe. The Layout Builder integration captures state for the _modal_ render but doesn't offer the upstream split-screen builder preview. Gap.
- **Preview for non-Page pageables** — `RenderPagePreviewSnapshotAction` is hard-bound to `Capell\Core\Models\Page`; `StoreLayoutBuilderPreviewStateAction` already accepts the broader `Pageable` contract, so the create/render side is the only thing blocking previewing other pageable models. Gap.

## 4. Issues / Risks

- **Upstream maintenance/version risk.** — Hard dependency on `pboivin/filament-peek: ^4.0` (`composer.json`). The package is pinned to that author's Filament-version cadence; a Filament major that the upstream lags on will block Capell. The Capell surface area touching upstream is tiny (`Peek::registerPreviewModal()`, `FilamentPeekPlugin::make()`, the `open-preview-modal` event — see `PeekPagePreviewAction.php:49`, `FilamentPeekPanelExtender.php:15`), so a fallback/vendored modal is feasible if upstream stalls. Document the blast radius. — `composer.json`, `src/Filament/Extenders/FilamentPeekPanelExtender.php`
- **Done / Shipped: real health-check probes replaced the stub.** — Evidence: `FilamentPeekHealthCheck::runDiagnostics()` now checks the signed preview route, snapshot/render action resolution, upstream `pboivin/filament-peek` availability, and configured preview cache-store reachability; `FilamentPeekHealthCheckTest` covers the healthy path, an invalid cache-store failure, and diagnostic output that avoids leaking snapshot/cache-key data. — `src/Health/FilamentPeekHealthCheck.php`, `tests/Unit/FilamentPeekHealthCheckTest.php`, `capell.json`
- **Done / Shipped: Layout Builder preview-state contract.** — The cross-package state store now implements `StoresLayoutBuilderPreviewState`, and Layout Builder verifies the resolved optional action implements that contract before invoking `handle()` or `clear()`. This locks the `handle`/`clear` signatures and keeps the single-argument clear path covered. — `src/Contracts/StoresLayoutBuilderPreviewState.php`, `src/Actions/StoreLayoutBuilderPreviewStateAction.php`, `../layout-builder/src/Livewire/Filament/Concerns/ManagesLayoutBuilderState.php`, `tests/Unit/SnapshotActionTest.php`
- **Preview auth depends on `update` ability everywhere — correct, but double-edged.** — Both `PeekPagePreviewAction` (`->authorize(... Gate::allows('update', $record))`) and `RenderPagePreviewSnapshotAction::renderSnapshot()` (`Gate::authorize('update', $page)`) gate on `update`. That's the right default and is covered by the "another user" + "unsigned" tests, but it means **a user who can edit a page but has narrower view scopes still renders the full public page** through the preview. Confirm no field/section-level authorization is bypassed by the render path. — `src/Http/Controllers/PagePreviewController.php`, `src/Actions/RenderPagePreviewSnapshotAction.php`
- **Done / Shipped: public-output-safety render rejection is covered.** — Evidence: `PagePreviewRouteTest` now rejects validly-signed preview URLs for unauthenticated requests and for authenticated non-admin snapshot owners before the preview renderer output appears; `PagePreviewController` treats render-path authorization failures as private 403 responses instead of generic render errors. — `tests/Feature/PagePreviewRouteTest.php`, `src/Http/Controllers/PagePreviewController.php`
- **Done / Shipped: `Cache-Control` literal is aligned.** — Evidence: `PagePreviewController::makePrivate()` and the preview route tests now use the same literal, `no-store, private`, for private preview and friendly error responses. — `src/Http/Controllers/PagePreviewController.php`, `tests/Feature/PagePreviewRouteTest.php`
- **i18n is solid for messages, thin elsewhere.** — User-facing strings are translated (`resources/lang/en/actions.php`, `errors.php`) and the error Blade only echoes translated `$title`/`$body` (no leaked internals — good). But the error view hard-codes `<html lang="en">` (`resources/views/preview-error.blade.php:2`) regardless of the editor's locale, and only `en` translations ship. Localize the `lang` attribute and add at least one second locale to prove the translation keys are real. — `resources/views/preview-error.blade.php`, `resources/lang/`
- **Done / Shipped: `CHANGELOG.md` and proprietary `LICENSE` are present.** — Evidence: `CHANGELOG.md` tracks the Filament Peek cleanup work under `Unreleased`, and `LICENSE` now matches the Composer manifest's proprietary license declaration. — package root
- **Test gaps (enumerated).** Covered: snapshot create/find/TTL/ownership (`SnapshotActionTest`, `PeekPagePreviewActionTest`), unsigned/expired/cross-user/unauthenticated/non-admin route rejection, unsaved name/title/body + featured-image/social-image overlay render, expired and render-failure friendly responses, provider extender wiring + "not installed" short-circuit (`FilamentPeekProviderTest`), upstream device-preset config, health-check probes (`FilamentPeekHealthCheckTest`), Layout Builder widget+asset registration (`RegisterLayoutBuilderPreviewWidgetsActionTest`), and the typed preview-state contract. Remaining useful depth: `withWorkspaceContext()` path, multi-language `currentTranslation` selection, and query-count/performance budget. — `tests/`

## 5. Marketplace & Positioning

This is a **foundation/bundled** package (`product.tier: free`, `bundle: foundation`, `commercial.proposedLicense: free`, `requestedCertification: first-party`). Functionally it's a thin wrapper _shell_ (upstream modal) around a substantial first-party engine (snapshot + frontend-overlay render). It earns a marketplace listing on the strength of the Capell-specific unsaved-state preview, **but** it should be positioned as a bundled Foundation capability, not a standalone paid add-on — its value is only realized alongside `admin` + `frontend` (hard requires) and is amplified by `layout-builder`/`publishing-studio`. If the marketplace has a "platform feature, included" lane, it belongs there rather than as an independent SKU.

- **Done / Shipped: `capell.json` `summary` was rewritten.** — Evidence: `capell.json` now leads with the editor outcome: preview unsaved page and Layout Builder edits through the live theme via a private, expiring, signed link, with nothing written until save.
- **Done / Shipped: Composer `description` was rewritten.** — Evidence: `composer.json` now describes private, expiring previews of unsaved Page and Layout Builder changes rendered through the live frontend pipeline without persisting draft state.
- **Screenshot contract partially closed.** `capell.json` now promotes real Capell runner PNG captures for the Page edit preview action group in light and dark admin themes, replacing the broken blank/loading product PNGs. The signed preview modal and expired-preview error entries are retained as optional runner follow-ups until the runner supports sequential grouped-action clicks and temporary signed route URL generation.
- **Platform-pitch contribution.** Reinforces Capell's "safe editing" story: edits are previewable before they're public, and (per `docs/`) preview state is provably never written into Pages/Layouts/translations/workspaces. Pairs in the pitch with publishing-studio (workspace review) and layout-builder (visual authoring) as the "see-before-you-ship" trio.
- **Keywords/tags (8–12):** `page preview`, `unsaved changes preview`, `draft preview`, `signed preview link`, `live theme preview`, `layout builder preview`, `editorial workflow`, `filament admin`, `content safety`, `no-persistence preview`, `responsive preview`, `cms preview`.

## 6. Prioritized Roadmap

| Item                                                                                                                                                                                                                     | Bucket | Effort | Impact | Section ref |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | ------ | ------ | ----------- |
| Shipped: Real health-check probes (routes/actions/Peek/cache) replaced the stub — evidence: `FilamentPeekHealthCheckTest` covers healthy diagnostics, invalid cache-store failure, and non-leaking messages.             | Done   | S      | High   | §4          |
| Shipped: Add unauthenticated + non-admin render-rejection tests (public-safety rule) — evidence: `PagePreviewRouteTest` covers signed anonymous rejection and signed non-admin owner rejection before renderer output.   | Done   | S      | High   | §4          |
| Future runner follow-up: real Capell runner product screenshots for the Page edit preview action group are promoted in light/dark mode; signed modal and expired-preview captures remain runner-tooling follow-ups.      | Future | S      | Med    | §1, §5      |
| Shipped: Rewrite `summary` + composer `description` — evidence: `capell.json` marketplace summary and package description plus `composer.json` description now lead with unsaved live-theme previews and no persistence. | Done   | S      | Med    | §5          |
| Shipped: Align `Cache-Control` literal between controller and test — evidence: controller and route assertions use `no-store, private`.                                                                                  | Done   | S      | Med    | §4          |
| Shipped: Remove dead `PagePreviewSnapshotData::$path` — evidence: DTO and snapshot creation no longer contain or write the field.                                                                                        | Done   | S      | Med    | §2          |
| Shipped: Add `CHANGELOG.md` + `LICENSE` (proprietary) — evidence: both package-root files exist and the changelog records this cleanup.                                                                                  | Done   | S      | Med    | §4          |
| Shipped 2026-06-08: formalize the layout-builder ↔ filament-peek action contract with `StoresLayoutBuilderPreviewState`.                                                                                                 | Done   | M      | High   | §4          |
| Shipped: Extract `find()`/snapshot read into `FindPagePreviewSnapshotAction`                                                                                                                                             | Done   | M      | Med    | §2          |
| Shipped: De-dup `currentUser()`/`cacheStore()`/`ttlMinutes()` into `ResolvesPreviewContext`                                                                                                                              | Done   | S      | Med    | §2          |
| Shipped 2026-06-08: expose responsive/device-frame preview through package config and upstream Peek modal config.                                                                                                        | Done   | M      | High   | §2, §3      |
| Shipped 2026-06-08: "Unsaved preview" ribbon on private preview responses.                                                                                                                                               | Done   | M      | Med    | §2          |
| Shipped 2026-06-06: Snapshot payload size guard + log                                                                                                                                                                    | Done   | S      | Med    | §2          |
| Shipped 2026-06-08: backfill render-path tests for render-failure 500 responses, social-image overlay, ribbon output, responsive config, and the typed preview-state contract.                                           | Done   | M      | High   | §4          |
| Shareable read-only preview token (reconcile with publishing-studio)                                                                                                                                                     | Later  | L      | High   | §3          |
| Live/auto-refresh preview + upstream builder-editor mode                                                                                                                                                                 | Later  | L      | Med    | §3          |
| Generalize preview to non-Page pageables                                                                                                                                                                                 | Later  | M      | Med    | §3          |
| Workspace-context and multilingual preview branch coverage                                                                                                                                                               | Later  | M      | Med    | §4          |
| Preview-render query/performance budget + assertion                                                                                                                                                                      | Later  | M      | Med    | §2, §4      |