Conversation
The network was declared as `net` and only became shop_flow_net because the project happened to be named shop_flow. An explicit `name:` removes that coincidence, so admin/ and shop/ can join it as an external network whatever project creates it. The pg_isready and redis-cli healthchecks let the app containers wait for real readiness rather than for the container merely to exist. Also drops a top-level `pgdata` volume declaration that nothing referenced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
`app` and `webserver` collide with admin's services once the root compose.yaml merges both files into one project: Compose `include` silently keeps the first definition and drops the second rather than reporting a conflict. Container names are unchanged. They now interpolate SHOP_CONTAINER_PREFIX, because COMPOSE_PROJECT_NAME is reserved and under the root project resolves to shop_flow for this file and admin/ alike, which made both nginx containers claim the same name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
`app` and `webserver` collide with the storefront's services once the root compose.yaml merges both files into one project: Compose `include` silently keeps the first definition and drops the second rather than reporting a conflict. Container names are unchanged. They now interpolate ADMIN_CONTAINER_PREFIX, because COMPOSE_PROJECT_NAME is reserved and under the root project resolves to shop_flow for this file and shop/ alike, which made both nginx containers claim the same name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
The compose file falls back to the previous value when it is absent, so existing .env files keep working; this only makes it discoverable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
The compose file falls back to the previous value when it is absent, so existing .env files keep working; this only makes it discoverable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
fastcgi_pass referenced `app`, which stopped resolving when the service was renamed, and nginx crash-looped on "host not found in upstream". Keeping an `app` network alias instead is not an option: under the root compose project both applications' php-fpm containers would answer to it on the same network, and admin requests would round-robin into the storefront. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
fastcgi_pass referenced `app`, which stopped resolving when the service was renamed, and nginx crash-looped on "host not found in upstream". Keeping an `app` network alias instead is not an option: under the root compose project both applications' php-fpm containers would answer to it on the same network, and storefront requests would round-robin into the panel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
`docker compose up -d --build` at the root now brings up the shared Postgres and Redis plus both applications. Each app keeps its own compose file so it can still be started on its own from <app>/docker/. This file only merges the three and adds what cannot live in them: the network ownership, and depends_on gating the apps on healthy db/redis, which is inexpressible in admin/ or shop/ alone because db is not part of those projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
vendor/, node_modules/ and the build output are all reproduced inside the image, so shipping the host's copies would only invalidate layers. Also keeps .env files out of the context. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
vendor/, node_modules/ and the build output are all reproduced inside the image, so shipping the host's copies would only invalidate layers. Also keeps .env files out of the context. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
The image is immutable, so opcache timestamp validation is pure overhead and is turned off. max_accelerated_files is raised to 30000 because Filament loads far more classes than the 10000 default allows, and save_comments stays on because attributes are read via reflection. JIT is present but disabled: it gives little for a request/response workload and should be benchmarked before being switched on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
clear_env is off, without which php-fpm would discard the environment Compose passes in and hide every value in the app's env_file from PHP. Worker output and the slow log go to stderr so `docker compose logs` is the single place to look, and pm.max_requests recycles workers to bound the damage from any slow leak. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Only the front controller may execute: any other .php path returns 404 instead of being handed to the interpreter. TLS is terminated by the proxy in front, so this listens on plain 80 inside the Docker network. Content-hashed Vite output under /build is served immutable, and symlinks stay enabled because public/storage points into the uploads volume. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
The development entrypoint runs composer install, npm build and migrate on every container start, then clears the caches. In production that is backwards: startup would depend on the network, and two containers starting together would race on the schema. This builds config/event/view caches instead, which has to happen at start rather than at build time because it bakes in the runtime environment. route:cache is left out because routes/web.php still has two closure routes, which Laravel cannot serialize. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
The image is immutable, so opcache timestamp validation is pure overhead and is turned off, and the accelerated-file limit is raised well above the 10000 default. JIT is present but disabled: it gives little for a request/response workload and should be benchmarked before being switched on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
clear_env is off, without which php-fpm would discard the environment Compose passes in and hide every value in the app's env_file from PHP. Worker output and the slow log go to stderr so `docker compose logs` is the single place to look, and pm.max_requests recycles workers to bound the damage from any slow leak. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Only the front controller may execute: any other .php path returns 404 instead of being handed to the interpreter. TLS is terminated by the proxy in front, so this listens on plain 80 inside the Docker network. Content-hashed Vite output under /build is served immutable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
The development entrypoint runs composer install, npm build and migrate on every container start, then clears the caches. In production that is backwards: startup would depend on the network being reachable. This builds the config, route, event and view caches instead, which has to happen at start rather than at build time because it bakes in the runtime environment. The storefront never migrates: admin owns the schema. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Targets `app` (php-fpm with the application baked in) and `web` (nginx with only public/), so the two share every layer up to `app`. Dependencies are built on the same PHP base as the runtime, because the composer image lacks ext-intl and cannot resolve this app's platform requirements. opcache is deliberately absent from the extension list: PHP 8.5 links Zend OPcache in statically, so there is no shared module to build and installing it fails. apt retries are set because apt only warns when an index fails to download, which otherwise surfaces much later as a confusing "Unable to locate package". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Targets `app` (php-fpm), `web` (nginx with only public/) and `ssr` (node running the Inertia renderer), so all three share their common layers. The renderer gets its own target because Vite externalises npm dependencies from an SSR build, so the bundle still needs the production node_modules at runtime. The PHP image carries the bundle too, since Inertia's ensure_bundle_exists check runs on the PHP side. Dependencies are built on the same PHP base as the runtime so composer resolves platform requirements against the production extension set. opcache is absent from that list on purpose: PHP 8.5 links it in statically, so installing it as a shared module fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Source is baked into images rather than bind-mounted, Postgres and Redis sit on named volumes and publish no host ports, and Caddy is the only container listening on the public interface. The Inertia renderer runs as its own container so a crash restarts it instead of silently dropping the storefront to client-side rendering. Queue and scheduler containers are behind a `workers` profile because nothing queues a job or registers a schedule yet. The redis command is a single-line list on purpose: as a YAML folded scalar the more-indented flags keep their newlines and end up as unreachable lines after `exec`, which left the server with no password at all. Its healthcheck asserts an unauthenticated PING is rejected, since a successful authenticated PING also passes against a passwordless server and hid exactly that bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Caddy obtains and renews Let's Encrypt certificates itself, so the only requirement is working DNS and reachable ports 80/443. It also sets the X-Forwarded-* headers the applications read through trustProxies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Images are built before anything is stopped, then migrations run once from a throwaway admin container, and only then are the long-running containers replaced. Only admin migrates, because it owns the shared schema. Images are tagged with the deployed commit so a rollback has something to point at without rebuilding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Ordered checklist from DNS through backups, with the reasoning for the parts that are easy to get wrong: DNS has to be in place before Caddy starts, the Postgres credentials only apply when the volume is first created, and the Vite build is the memory peak on a small box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Holds only what Compose itself interpolates — database and Redis credentials, the two hostnames, and the image tag. The applications read their own env files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Redis for sessions and cache, stderr logging so nothing on disk needs rotating, and the public disk for uploads so they land in the mounted volume that nginx serves at /storage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Redis for sessions, because file sessions would be lost on every deploy when the container filesystem is replaced. INERTIA_SSR_URL points at the renderer container, and IMAGE_URL at the admin domain, which is what serves product images. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
In production the panel sits behind Caddy, which terminates TLS. Without this the application sees plain HTTP and generates http:// URLs and redirects. The storefront already had it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
It holds real database and Redis credentials. The per-app .gitignore files already cover admin/.env.production and shop/.env.production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
Getting started now runs one command from the root, with the container and port table, and points at infrastructure/production/ for deployment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tQqhzpVk2Kms4qqfhpZko
deploy.sh builds on the server, which needs Docker Hub, deb.debian.org, packagist and the npm registry all reachable — unusable on a host where those are filtered. ship-images.sh cross-builds the five app images plus the three pulled service images for linux/amd64 on a workstation, verifies every one is actually that architecture, and streams the lot through `docker save | ssh | docker load`. deploy-prebuilt.sh is the server-side counterpart: deploy.sh with the build step dropped and `--pull never` added, so a missing tag fails immediately instead of hanging on a registry that will never answer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
canAccessPanel() requires the super-admin or admin role, but make:filament-user assigns none — the account it creates cannot log in. Add the ADMIN_* variables AdminSeeder reads (config/admin.php), so a production deploy sets them instead of shipping the admin@shopFlow.dev / password defaults, and so the seeder — which does assign the role — has values to seed with. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Caddy's automatic HTTPS needs to reach acme-v02.api.letsencrypt.org directly from the VPS to request a certificate — unreachable on a host where ACME, and every other outbound path this stack depends on (Docker Hub, apt, npm), is filtered. Proxying through a CDN doesn't route around it either: a CDN's edge still has to reach this origin, and Cloudflare's cannot. TLS_CERT_FILE / TLS_KEY_FILE point Caddy at a certificate obtained elsewhere instead — a DNS-01 challenge run from a workstation that can reach both Let's Encrypt and the zone's DNS provider — and mounted read-only from infrastructure/production/certs/ (gitignored; holds a private key). certs/README.md documents issuing and renewing it. Also add SHOP_LEGACY_DOMAINS, a Caddy site block that 301-redirects old or alternate storefront hostnames to SHOP_DOMAIN, and TRUSTED_PROXIES, so X-Forwarded-Proto survives correctly if a CDN or LB ever does sit in front of Caddy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cover the new deploy path end to end: why deploy.sh's on-box build fails on a filtered host, how ship-images.sh / deploy-prebuilt.sh replace it, why Caddy needs a manually-issued certificate instead of automatic ACME, and the corrected first-admin-user step (AdminSeeder, not make:filament-user — the latter creates a user with no role, and the panel gate requires one). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The storefront never read `home_sections`. It always rendered a fixed layout, so reordering or disabling a row in the panel changed nothing on the site — the table, its resource, model, factory, seeder and enum were an elaborate no-op. What appears on the home page is controlled by banner/slider positions instead, which the storefront does read. The create migration is deleted so a fresh database never builds the table, and drop_home_sections_table removes it from databases that already ran it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A position value is an opaque slug — picking `product-side` from a dropdown was guesswork about where it would appear. Each position now carries a description, the storefront page it sits on, the aspect ratio that slot renders at and a recommended source size. The form renders a wireframe of the three storefront pages and highlights the slot the chosen position fills, mirrored into `data-selected` by Alpine so it keeps up with the radio without a server round-trip. Uploads are cropped to the slot's real ratio, so one oddly-shaped image cannot stretch the layout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`PermissionsEnum` was decorative: it named four post-shaped permissions nothing checked, while no resource declared any authorization at all. Filament therefore fell back to model policies, and with almost none registered every panel user had full access to everything — settings, gateway credentials and staff accounts included. Permissions are now a PermissionGroupEnum (catalog / content / orders / customers / shipping / marketing / settings) crossed with a PermissionActionEnum, so one grant covers every resource in an area instead of needing a permission per resource. Each resource opts in via `AuthorizesWithPermissions` and declares its group; `ResourcePermissionsTest` fails the build if one forgets, since an ungated resource is reachable by anyone who can open the panel. Where a policy exists it still applies on top, but only for the abilities it actually implements — Laravel denies any ability a policy omits, which would otherwise forbid viewing categories just because CategoryPolicy defines only delete. `login()` now seeds real roles instead of hand-rolling one, so tests authorize exactly as the panel does, and deploy.sh runs the seeder on every release to pick up newly added permissions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
It was the only relationship in the schema with no foreign key — a bare integer. Deleting a parent left its children pointing at a row that no longer existed, and because the storefront walks `parent_id` to collect descendants, an orphaned subtree silently vanished from category listings instead of failing loudly. The column is widened to bigint to match `categories.id`, any already-orphaned child is promoted to a root, and the constraint restricts on delete rather than cascading — cascading would take out an entire subtree and, through products, a great deal more. This matches `products.category_id`, which already restricted. CategoryPolicy now refuses to delete a category while children or products still point at it, so the panel stops offering a delete button that could only ever produce a raw constraint error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/ORDER.md states that stock is consumed from the moment an order is paid and released if it is canceled or returned. The storefront upheld that for gateway payments only. Every status change staff made in the panel — confirming a card-to-card receipt, canceling, accepting a return — left `varieties.inventory` untouched, so the two halves of the same invariant disagreed. `OrderStatusEnum::consumesStock()` is now the single definition of which statuses hold stock, and OrderObserver adjusts inventory on any move between the two sets. Only crossings act, so PAID -> SHIPPED and a plain re-save change nothing. Lines are row-locked in a consistent order, the same as DecrementInventoryAndMarkPaid. A transition that stock cannot cover is refused rather than pushing an unsigned column negative: EditOrder wraps the save in a transaction and surfaces which variety fell short, instead of a 500 over an order that had already moved. The observer is registered by the admin app only — the storefront has its own Order model, so a Zarinpal payment still decrements exactly once. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both copies of ORDER.md said inventory is never restocked for a return, which stopped being true when OrderObserver landed. Documents the consumesStock() rule, which transitions adjust stock, that the transition is refused when stock cannot cover it, and that a damaged return still needs a manual adjustment afterwards. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three of the seven positions an admin could pick had no render site at all: choosing `home-top`, `category-top`, `category-side` or `product-side` published content that appeared nowhere, with nothing in the panel to say so. `SliderSlot` and `BannerSlot` replace the home-only HeroSlider and BannerGrid. Both take the slot's data and an arrangement (hero / wide / portrait, grid / wide / stack), render nothing when the slot is empty so a page can hand them an empty array without guarding, and pin their own aspect ratio to match the ratio the admin crops to. The two lookup actions move to `App\Actions\Layout` since they now serve the home, category and product pages rather than just the home page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A featured tag rendered as an image card in a strip, which said nothing about what the tag actually contains. Each one is now a product carousel resolved with the same category-plus-attribute rules the tag's own page uses, so a row on the home page and the tag page always agree about what belongs to it. The grouping logic those rules need is extracted to `Actions\Catalog\GroupAttributeIds`, shared with the category listing so the facet rules (OR within an attribute group, AND across groups) cannot drift between the two. MAX_ROWS bounds the cost: each row is its own set of queries, so without it the home page's query count grew with however many tags staff featured. Tags matching no products are dropped rather than rendered as an empty carousel. ProductCarousel moves out of `Components/Home` now that the product page uses it too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The cart previewed a discount and checkout ignored it, so a customer was shown a saving and then charged full price — worse than not offering the coupon at all. `ResolveCartCoupon` now owns the session key and re-validates the code wherever a total is shown or charged, so the cart, the payment page and the amount actually captured cannot disagree. CreatePendingOrder records `coupon_id` and the saving on the order and zeroes shipping for a free-shipping coupon. `coupons.total_used` is incremented in DecrementInventoryAndMarkPaid rather than at order creation: an abandoned or failed order must not burn one of a coupon's uses. If the code has stopped validating between the cart and the payment click, the customer goes back to the cart with the reason instead of being silently charged the undiscounted total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every storefront enum has a twin in the admin app, and the backing values are what the two apps agree on through the shared database. Changing one side alone breaks the other silently — staff save a value the storefront never recognises, with no error anywhere. Asserts every mirrored enum has identical case names and values, and that a new storefront enum has an admin counterpart at all. Also asserts the comparison actually read the admin enums, so the test cannot pass by finding nothing to check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude Code reads CLAUDE.md, never AGENTS.md. So the 160 lines of admin
conventions and 187 lines of storefront conventions written in the two
AGENTS.md files were never reaching a session, while admin/CLAUDE.md
auto-loaded 400 lines duplicating the same Boost block — and
contradicting it, telling the reader to use `php artisan test` where
AGENTS.md says to use Pest directly. The wrong instruction was the one
in effect.
The conventions now live in ai-context/claude/{admin,shop}.md, imported
by admin/CLAUDE.md and shop/CLAUDE.md. Nested CLAUDE.md files load on
demand, so a storefront task no longer pays for Filament conventions and
an admin task no longer pays for Inertia/SSR ones; only the repo-wide
file loads every session. Imports resolve relative to the importing
file, hence `@../ai-context/...`.
Both AGENTS.md files are generated by `boost:install`, which would have
wiped hand-written content on its next run; moving the conventions out
puts them beyond its reach. They keep a pointer line, and other
assistants that do read AGENTS.md still find Boost's own guidance.
Not a submodule, unlike the HBOX setup this mirrors: one repo with one
version line has no second consumer and no per-major branch, so a plain
directory does the same job without the uninitialised-clone trap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The schema reference still documented a `home_sections` table that was dropped, still said only `home-main` and `home-middle` were rendered when every banner and slider position now has a render site, described `categories.parent_id` as a plain column after it gained a restricting foreign key, and described coupons as preview-only after checkout began committing them. Both IMPLEMENTATION.md copies still listed Home Sections as built. Also records the gap found while checking: `coupons.total_used` is incremented only on gateway payment, so an order staff mark paid in the panel does not count a use. Written down under `orders` rather than left in a chat, since the rule to apply is still undecided. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The code was generated and cached but only ever written to the log, so nobody could actually log in with a phone they did not control. Delivery goes through the `SmsSender` contract: `SmsIrSender` posts to sms.ir's `send/verify`, `LogSmsSender` keeps the old logging. The binding is chosen by whether `services.sms_ir.api_key` is filled, so production opts in by setting the key while a clone with no credentials logs the code and spends nothing — real sending can never happen by accident. `send/verify` rather than `send/bulk`: verify rides sms.ir's shared service line, so it needs no rented number, reaches customers who blocked advertising SMS at their operator, and is not restricted at night. A login code sent as bulk would be dropped for some customers entirely. Two details worth keeping: - `SendOtpCode` now sends *before* it caches and returns null on refusal. Caching a code the customer never receives would lock them out for the whole TTL, unable to request another, waiting for an SMS that does not exist. Both controllers turn that null into an error on the mobile step. - The expiry in the message is formatted in Asia/Tehran, not the app's UTC. A customer whose clock reads 14:35 being told the code is valid until 11:05 would assume it had already died. One expiry instant is threaded through the SMS text, the stored expiry and the cache lifetime so they cannot drift apart. Parameter names match the panel template exactly (`code`, `expireDate`); a placeholder left without a value reaches the customer as the literal `#expireDate#`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reading production logs meant `docker exec ... tail`, and there was no answer at all to "is the server healthy" or "what is slow". Pulse records from **both** apps into the same `pulse_*` tables, since they share one database — a dashboard covering only the panel would miss the storefront, which takes the real traffic. The storefront gets the package but `config/pulse.php` there sets `path` to null, so no `/pulse` route exists on a public site; the dashboard lives here, and the migrations live here too because admin owns the schema. Neither dashboard is a Filament resource, so AuthorizesWithPermissions does not reach them, and both ship open by default while showing slow queries, exception messages and whole stack traces. The `viewPulse` and `viewLogViewer` gates restrict them to super-admin — `admin` is deliberately not enough — and OperationalDashboardsTest fails the build if either opens up, including for a storefront customer, who is a real row in the shared `users` table. Logging becomes `stack` = `stderr,shared`. stderr keeps `docker logs` and Docker's rotation exactly as they were; `shared` adds the rotating file a viewer can actually read, since a file-based viewer against `LOG_CHANNEL=stderr` would have shown an empty page in production. The two apps write into one named volume, each in its own directory, because the panel cannot see into another container — the viewer then lists them as two folders rather than one mixed stream. Both Dockerfiles create /var/log/shopflow owned by www-data. Docker initialises a fresh named volume from the image including ownership; left to Docker the volume is root-owned and php-fpm silently writes nothing at all. `pulse:check` runs as its own container to feed the Servers card. Note `pulse:trim` runs from the scheduler, which is still behind the workers profile, so the pulse_* tables grow until that is started. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both register their own routes outside Filament, so nothing in the panel pointed at them — you had to know the URLs and type them by hand. Two sidebar links, in the top section beside Dashboard and Users rather than filed under a heading at the bottom. They open in a new tab, so the panel stays where it was in the original tab. Links rather than pages embedding them: each dashboard is a full-page app with its own stylesheet and breakpoints, and inside a panel frame their layout collapsed into an unreadable overlap. Keeping them full-width is worth more than keeping them inside the panel chrome. Visibility follows the same viewPulse / viewLogViewer gates the routes use, so a plain admin never sees a link they cannot open — the sidebar must not become a way around the gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both cost time in one session. The remote shell one is nastier than it looks: a bash loop sent as an ssh argument fails only when it reaches the remote, so the earlier steps of a deploy script have already run by then. The Filament one is invisible in the other direction — the code is right, the tests pass, and the sidebar still shows nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sms.ir's sandbox key accepts a send and delivers nothing, so with it configured nobody can finish a login — the code exists only in Redis. This lets a listed mobile use a known code instead, so the site is usable while the production template is still pending approval. It is an authentication bypass and is written to be hard to leave running by accident: off unless OTP_FIXED_CODE is set, limited to the mobiles in OTP_FIXED_MOBILES, and logged on every use — `warning` when scoped to numbers, `critical` when no allowlist is set, since that case means every account on the site can be entered with one known code. The allowlist is the part that matters: without it a stranger could type their own number on a public storefront and be signed in, and first login creates the account. Tests cover that an unlisted mobile still gets a real random code and a real send. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…renders at Category, page, product, tag and variety images had no crop ratio, so an odd upload could stretch a circle, blow out a page hero with no height frame, or drift the product form's variety photo out of step with the standalone variety form's 1:1. ImageAspectEnum centralizes the ratio per slot (null on purpose for brand/gateway logos, drawn with object-contain, and for receipts, where a crop could cut off the reference number or amount). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a fourth checkout-critical shipping method alongside the existing courier/post/pickup rows: nationwide, pay_on_delivery=true, amount=null — the freight is settled with the courier at delivery, same pattern as the existing in-store pickup row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing them A product with several values in one attribute group (e.g. five shoe sizes) rendered as five identical "سایز کفش" rows instead of one row listing all five. Groups specs server-side by attribute group name; each group now carries its full value list plus a highlight flag, so a highlighted spec (e.g. material) gets visual emphasis instead of sitting unused — `highlights` was computed but never actually rendered anywhere in the frontend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds DemoSeeder, reading only demo/data/*.json (no network calls, idempotent, guarded against production): ~50 original products with variants/images, categories, attributes, brands, homepage banners/ sliders, tags, a header menu, and reviews with dedicated demo-reviewer accounts. Wired into DatabaseSeeder for local/staging only. Images are Pexels stock photos (commercial-license, not scraped), fetched by demo/scripts/ (tracked) into admin/storage/app/public/demo/ (gitignored, regenerated per-machine — see demo/README.md for why and how). demo/data/*.json itself is tracked; only the fetch cache and the Pexels API key are excluded. CategorySeeder/AncestorSeeder/AttributeSeeder/AttributeGroupCategorySeeder stay commented out in DatabaseSeeder for now (kept as-is, not part of this change). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.